diff --git a/CHANGELOG.md b/CHANGELOG.md index 877029fb0..cb3fc02c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [2.9.2] - Injects custom styling for the HTML panel used by Snyk Code for consistent ignores. +- Add warning messages in the Tree View for the issue view options used in consistent ignores. ## [2.8.1] - Lower the strictness of custom endpoint regex validation so that single tenant APIs are allowed. diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 158b75982..c4df23354 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -38,6 +38,8 @@ export type FeaturesConfiguration = { export interface IssueViewOptions { ignoredIssues: boolean; openIssues: boolean; + + [option: string]: boolean; } export interface SeverityFilter { diff --git a/src/snyk/common/messages/analysisMessages.ts b/src/snyk/common/messages/analysisMessages.ts index b54d200f8..63f555ae5 100644 --- a/src/snyk/common/messages/analysisMessages.ts +++ b/src/snyk/common/messages/analysisMessages.ts @@ -4,6 +4,9 @@ export const messages = { clickToProblem: 'Click here to see the problem.', scanRunning: 'Scanning...', allSeverityFiltersDisabled: 'Please enable severity filters to see the results.', + allIssueViewOptionsDisabled: 'Adjust your Issue View Options to see all issues.', + openIssueViewOptionDisabled: 'Adjust your Issue View Options to see open issues.', + ignoredIssueViewOptionDisabled: 'Adjust your Issue View Options to see ignored issues.', 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.', diff --git a/src/snyk/common/views/analysisTreeNodeProvider.ts b/src/snyk/common/views/analysisTreeNodeProvider.ts index 8cea9f592..264ca87a4 100644 --- a/src/snyk/common/views/analysisTreeNodeProvider.ts +++ b/src/snyk/common/views/analysisTreeNodeProvider.ts @@ -2,10 +2,13 @@ import _ from 'lodash'; import * as path from 'path'; import { AnalysisStatusProvider } from '../analysis/statusProvider'; import { IConfiguration } from '../configuration/configuration'; -import { SNYK_SHOW_LS_OUTPUT_COMMAND } from '../constants/commands'; +import { SNYK_SHOW_LS_OUTPUT_COMMAND, VSCODE_GO_TO_SETTINGS_COMMAND } from '../constants/commands'; import { messages } from '../messages/analysisMessages'; import { NODE_ICONS, TreeNode } from './treeNode'; import { TreeNodeProvider } from './treeNodeProvider'; +import { SNYK_NAME_EXTENSION, SNYK_PUBLISHER } from '../constants/general'; +import { configuration } from '../configuration/instance'; +import { FEATURE_FLAGS } from '../constants/featureFlags'; export abstract class AnalysisTreeNodeProvider extends TreeNodeProvider { constructor(protected readonly configuration: IConfiguration, private statusProvider: AnalysisStatusProvider) { @@ -47,6 +50,51 @@ export abstract class AnalysisTreeNodeProvider extends TreeNodeProvider { }); } + protected getNoIssueViewOptionsSelectedTreeNode(numIssues: number, ignoredIssueCount: number): TreeNode | null { + const isIgnoresEnabled = configuration.getFeatureFlag(FEATURE_FLAGS.consistentIgnores); + if (!isIgnoresEnabled) { + return null; + } + + const anyOptionEnabled = Object.values(this.configuration.issueViewOptions).find(enabled => !!enabled); + if (!anyOptionEnabled) { + return new TreeNode({ + text: messages.allIssueViewOptionsDisabled, + }); + } + + if (numIssues === 0) { + return null; + } + + // if only ignored issues are enabled, then let the customer know to adjust their settings + if (numIssues === ignoredIssueCount && !this.configuration.issueViewOptions.ignoredIssues) { + return new TreeNode({ + text: messages.ignoredIssueViewOptionDisabled, + command: { + command: VSCODE_GO_TO_SETTINGS_COMMAND, + title: '', + arguments: [`@ext:${SNYK_PUBLISHER}.${SNYK_NAME_EXTENSION}`], + }, + }); + } + + // if only open issues are enabled, then let the customer know to adjust their settings + if (ignoredIssueCount === 0 && !this.configuration.issueViewOptions.openIssues) { + return new TreeNode({ + text: messages.openIssueViewOptionDisabled, + command: { + command: VSCODE_GO_TO_SETTINGS_COMMAND, + title: '', + arguments: [`@ext:${SNYK_PUBLISHER}.${SNYK_NAME_EXTENSION}`], + }, + }); + } + + // if all options are enabled we don't want to show a warning + return null; + } + protected getErrorEncounteredTreeNode(scanPath?: string): TreeNode { return new TreeNode({ icon: NODE_ICONS.error, diff --git a/src/snyk/common/views/issueTreeProvider.ts b/src/snyk/common/views/issueTreeProvider.ts index 77e07b2ac..ade44e095 100644 --- a/src/snyk/common/views/issueTreeProvider.ts +++ b/src/snyk/common/views/issueTreeProvider.ts @@ -85,13 +85,25 @@ export abstract class ProductIssueTreeProvider extends AnalysisTreeNodeProvid nodes.sort(this.compareNodes); + const totalIssueCount = this.getTotalIssueCount(); + const ignoredIssueCount = this.getIgnoredCount(); + const topNodes: (TreeNode | null)[] = [ new TreeNode({ - text: this.getIssueFoundText(this.getTotalIssueCount()), + text: this.getIssueFoundText(totalIssueCount, ignoredIssueCount), }), this.getFixableIssuesNode(this.getFixableCount()), - this.getNoSeverityFiltersSelectedTreeNode(), ]; + const noSeverityFiltersSelectedWarning = this.getNoSeverityFiltersSelectedTreeNode(); + if (noSeverityFiltersSelectedWarning !== null) { + topNodes.push(noSeverityFiltersSelectedWarning); + } else { + const noIssueViewOptionSelectedWarning = this.getNoIssueViewOptionsSelectedTreeNode( + totalIssueCount, + ignoredIssueCount, + ); + topNodes.push(noIssueViewOptionSelectedWarning); + } nodes.unshift(...topNodes.filter((n): n is TreeNode => n !== null)); return nodes; @@ -103,7 +115,9 @@ export abstract class ProductIssueTreeProvider extends AnalysisTreeNodeProvid getFilteredIssues(): Issue[] { const folderResults = Array.from(this.productService.result.values()); - const successfulResults = flatten(folderResults.filter((result): result is Issue[] => Array.isArray(result))); + const successfulResults = flatten>( + folderResults.filter((result): result is Issue[] => Array.isArray(result)), + ); return this.filterIssues(successfulResults); } @@ -115,6 +129,11 @@ export abstract class ProductIssueTreeProvider extends AnalysisTreeNodeProvid return this.getFilteredIssues().filter(issue => this.isFixableIssue(issue)).length; } + getIgnoredCount(): number { + const ignoredIssues = this.getFilteredIssues().filter(issue => issue.isIgnored); + return ignoredIssues.length; + } + isFixableIssue(_issue: Issue) { return false; // optionally overridden by products } @@ -255,7 +274,7 @@ export abstract class ProductIssueTreeProvider extends AnalysisTreeNodeProvid return nodes; } - protected getIssueFoundText(nIssues: number): string { + protected getIssueFoundText(nIssues: number, _: number): string { return `Snyk found ${!nIssues ? 'no issues! ✅' : `${nIssues} issue${nIssues === 1 ? '' : 's'}`}`; } diff --git a/src/snyk/snykCode/views/securityIssueTreeProvider.ts b/src/snyk/snykCode/views/securityIssueTreeProvider.ts index 89ed80ca8..f1d30ee1b 100644 --- a/src/snyk/snykCode/views/securityIssueTreeProvider.ts +++ b/src/snyk/snykCode/views/securityIssueTreeProvider.ts @@ -8,6 +8,7 @@ import { IViewManagerService } from '../../common/services/viewManagerService'; import { TreeNode } from '../../common/views/treeNode'; import { IVSCodeLanguages } from '../../common/vscode/languages'; import { IssueTreeProvider } from './issueTreeProvider'; +import { FEATURE_FLAGS } from '../../common/constants/featureFlags'; export default class CodeSecurityIssueTreeProvider extends IssueTreeProvider { constructor( @@ -38,9 +39,19 @@ export default class CodeSecurityIssueTreeProvider extends IssueTreeProvider { return `${dir} - ${issueCount} ${issueCount === 1 ? 'vulnerability' : 'vulnerabilities'}`; } - protected getIssueFoundText(nIssues: number): string { + protected getIssueFoundText(nIssues: number, ignoredIssueCount: number): string { if (nIssues > 0) { - return nIssues === 1 ? `${nIssues} vulnerability found by Snyk` : `✋ ${nIssues} vulnerabilities found by Snyk`; + let text; + if (nIssues === 1) { + text = `${nIssues} vulnerability found by Snyk`; + } else { + text = `✋ ${nIssues} vulnerabilities found by Snyk`; + } + const isIgnoresEnabled = configuration.getFeatureFlag(FEATURE_FLAGS.consistentIgnores); + if (isIgnoresEnabled) { + text += `, ${ignoredIssueCount} ignored`; + } + return text; } else { return '✅ Congrats! No vulnerabilities found!'; } diff --git a/src/snyk/snykIac/views/iacIssueTreeProvider.ts b/src/snyk/snykIac/views/iacIssueTreeProvider.ts index b34c02663..9af547e99 100644 --- a/src/snyk/snykIac/views/iacIssueTreeProvider.ts +++ b/src/snyk/snykIac/views/iacIssueTreeProvider.ts @@ -48,7 +48,7 @@ export default class IacIssueTreeProvider extends ProductIssueTreeProvider { + let contextService: IContextService; + let codeService: IProductService; + let languages: IVSCodeLanguages; + + let issueTreeProvider: IssueTreeProvider; + + setup(() => { + contextService = { + shouldShowCodeAnalysis: true, + } as unknown as IContextService; + codeService = { + isLsDownloadSuccessful: true, + isAnyWorkspaceFolderTrusted: true, + isAnalysisRunning: false, + isAnyResultAvailable: () => true, + result: { + values: () => [[]], + }, + } as unknown as IProductService; + configuration.setFeatureFlag(FEATURE_FLAGS.consistentIgnores, true); + languages = {} as unknown as IVSCodeLanguages; + }); + + teardown(() => { + sinon.restore(); + }); + + test('getRootChildren returns no extra root children', () => { + const localCodeService = { + ...codeService, + result: { + values: () => [ + [ + { + filePath: '//folderName//test.js', + isIgnored: false, + additionalData: { + rule: 'some-rule', + hasAIFix: false, + isSecurityType: true, + }, + } as unknown as CodeIssueData, + ], + ], + }, + } as unknown as IProductService; + + issueTreeProvider = new IssueTreeProvider(contextService, localCodeService, configuration, languages, true); + + sinon.stub(issueTreeProvider, 'getResultNodes').returns([]); + const rootChildren = issueTreeProvider.getRootChildren(); + strictEqual(rootChildren.length, 2); + strictEqual(rootChildren[0].label, 'Snyk found 1 issue'); + strictEqual(rootChildren[1].label, 'There are no vulnerabilities fixable by Snyk DeepCode AI'); + }); + + test('getRootChildren returns a root child for no results', () => { + const localCodeService = { + ...codeService, + result: { + values: () => [[]], + }, + } as unknown as IProductService; + + issueTreeProvider = new IssueTreeProvider(contextService, localCodeService, configuration, languages, true); + + sinon.stub(issueTreeProvider, 'getResultNodes').returns([]); + const rootChildren = issueTreeProvider.getRootChildren(); + strictEqual(rootChildren.length, 2); + strictEqual(rootChildren[0].label, 'Snyk found no issues! ✅'); + strictEqual(rootChildren[1].label, 'There are no vulnerabilities fixable by Snyk DeepCode AI'); + }); + + test('getRootChildren returns a root child for only open but not visible issues', async () => { + const localCodeService = { + ...codeService, + result: { + values: () => [ + [ + { + filePath: '//folderName//test.js', + additionalData: { + rule: 'some-rule', + hasAIFix: false, + isIgnored: false, + isSecurityType: true, + }, + } as unknown as CodeIssueData, + ], + ], + }, + } as unknown as IProductService; + + await vscode.workspace.getConfiguration().update(ISSUE_VIEW_OPTIONS_SETTING, { + openIssues: false, + ignoredIssues: true, + }); + configuration.issueViewOptions.openIssues = false; + issueTreeProvider = new IssueTreeProvider(contextService, localCodeService, configuration, languages, true); + + sinon.stub(issueTreeProvider, 'getResultNodes').returns([]); + const rootChildren = issueTreeProvider.getRootChildren(); + strictEqual(rootChildren.length, 3); + strictEqual(rootChildren[0].label, 'Snyk found 1 issue'); + strictEqual(rootChildren[1].label, 'There are no vulnerabilities fixable by Snyk DeepCode AI'); + strictEqual(rootChildren[2].label, 'Adjust your Issue View Options to see open issues.'); + await vscode.workspace.getConfiguration().update(ISSUE_VIEW_OPTIONS_SETTING, { + openIssues: true, + ignoredIssues: true, + }); + }); + + test('getRootChildren returns a root child for only ignored but not visible issues', async () => { + const localCodeService = { + ...codeService, + result: { + values: () => [ + [ + { + filePath: '//folderName//test.js', + isIgnored: true, + additionalData: { + rule: 'some-rule', + hasAIFix: false, + isSecurityType: true, + }, + } as unknown as CodeIssueData, + ], + ], + }, + } as unknown as IProductService; + + await vscode.workspace.getConfiguration().update(ISSUE_VIEW_OPTIONS_SETTING, { + openIssues: true, + ignoredIssues: false, + }); + issueTreeProvider = new IssueTreeProvider(contextService, localCodeService, configuration, languages, true); + + sinon.stub(issueTreeProvider, 'getResultNodes').returns([]); + const rootChildren = issueTreeProvider.getRootChildren(); + strictEqual(rootChildren.length, 3); + strictEqual(rootChildren[0].label, 'Snyk found 1 issue'); + strictEqual(rootChildren[1].label, 'There are no vulnerabilities fixable by Snyk DeepCode AI'); + strictEqual(rootChildren[2].label, 'Adjust your Issue View Options to see ignored issues.'); + await vscode.workspace.getConfiguration().update(ISSUE_VIEW_OPTIONS_SETTING, { + openIssues: true, + ignoredIssues: true, + }); + }); + + test('getRootChildren returns a root child for no visible issues', async () => { + const localCodeService = { + ...codeService, + result: { + values: () => [ + [ + { + filePath: '//folderName//test.js', + isIgnored: false, + additionalData: { + rule: 'some-rule', + hasAIFix: false, + isSecurityType: true, + }, + } as unknown as CodeIssueData, + ], + ], + }, + } as unknown as IProductService; + + await vscode.workspace.getConfiguration().update(ISSUE_VIEW_OPTIONS_SETTING, { + openIssues: false, + ignoredIssues: false, + }); + issueTreeProvider = new IssueTreeProvider(contextService, localCodeService, configuration, languages, true); + + sinon.stub(issueTreeProvider, 'getResultNodes').returns([]); + const rootChildren = issueTreeProvider.getRootChildren(); + strictEqual(rootChildren.length, 3); + strictEqual(rootChildren[0].label, 'Snyk found 1 issue'); + strictEqual(rootChildren[1].label, 'There are no vulnerabilities fixable by Snyk DeepCode AI'); + strictEqual(rootChildren[2].label, 'Adjust your Issue View Options to see all issues.'); + await vscode.workspace.getConfiguration().update(ISSUE_VIEW_OPTIONS_SETTING, { + openIssues: true, + ignoredIssues: true, + }); + }); +});