Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Derive Vulnerability Count from LS results [HEAD-942] #388

Merged
merged 13 commits into from
Nov 6, 2023
4 changes: 2 additions & 2 deletions src/snyk/base/modules/baseSnykModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { ICodeSettings } from '../../snykCode/codeSettings';
import SnykEditorsWatcher from '../../snykCode/watchers/editorsWatcher';
import { OssServiceLanguageServer } from '../../snykOss/ossServiceLanguageServer';
import { OssService } from '../../snykOss/services/ossService';
import { OssVulnerabilityCountService } from '../../snykOss/services/vulnerabilityCount/ossVulnerabilityCountService';
import { OssVulnerabilityCountServiceLS } from '../../snykOss/services/vulnerabilityCount/ossVulnerabilityCountServiceLS';
import { IAuthenticationService } from '../services/authenticationService';
import { ScanModeService } from '../services/scanModeService';
import SnykStatusBarItem, { IStatusBarItem } from '../statusBarItem/statusBarItem';
Expand All @@ -50,7 +50,7 @@ export default abstract class BaseSnykModule implements IBaseSnykModule {
protected advisorService?: AdvisorProvider;
protected commandController: CommandController;
protected scanModeService: ScanModeService;
protected ossVulnerabilityCountService: OssVulnerabilityCountService;
protected ossVulnerabilityCountServiceLanguageServer: OssVulnerabilityCountServiceLS;
protected advisorScoreDisposable: AdvisorService;
protected languageServer: ILanguageServer;

Expand Down
4 changes: 3 additions & 1 deletion src/snyk/common/services/productService.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -30,6 +30,7 @@ export interface IProductService<T> extends AnalysisStatusProvider, Disposable {

export abstract class ProductService<T> extends AnalysisStatusProvider implements IProductService<T> {
private _result: ProductResult<T>;
readonly newResultAvailable$ = new Subject<void>();

// Track running scan count. Assumption: server sends N success/error messages for N scans in progress.
private runningScanCount = 0;
Expand Down Expand Up @@ -168,6 +169,7 @@ export abstract class ProductService<T> extends AnalysisStatusProvider implement
this.analysisFinished();
this.runningScanCount = 0;

this.newResultAvailable$.next();
this.refreshTreeView();
}
}
Expand Down
16 changes: 8 additions & 8 deletions src/snyk/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ import { OssServiceLanguageServer } from './snykOss/ossServiceLanguageServer';
import { OssDetailPanelProvider } from './snykOss/providers/ossDetailPanelProvider';
import OssIssueTreeProvider from './snykOss/providers/ossVulnerabilityTreeProvider';
import { OssService } from './snykOss/services/ossService';
import { OssVulnerabilityCountService } from './snykOss/services/vulnerabilityCount/ossVulnerabilityCountService';
import { ModuleVulnerabilityCountProvider } from './snykOss/services/vulnerabilityCount/vulnerabilityCountProvider';
import { OssVulnerabilityCountServiceLS } from './snykOss/services/vulnerabilityCount/ossVulnerabilityCountServiceLS';
import { ModuleVulnerabilityCountProviderLS } from './snykOss/services/vulnerabilityCount/vulnerabilityCountProviderLS';
import { OssVulnerabilityTreeProvider } from './snykOss/views/ossVulnerabilityTreeProvider';
import { OssSuggestionWebviewProvider } from './snykOss/views/suggestion/ossSuggestionWebviewProvider';
import { DailyScanJob } from './snykOss/watchers/dailyScanJob';
Expand Down Expand Up @@ -414,24 +414,24 @@ class SnykExtension extends SnykLib implements IExtension {

this.initDependencyDownload();

this.ossVulnerabilityCountService = new OssVulnerabilityCountService(
this.ossVulnerabilityCountServiceLanguageServer = new OssVulnerabilityCountServiceLS(
vsCodeWorkspace,
vsCodeWindow,
vsCodeLanguages,
new ModuleVulnerabilityCountProvider(
this.ossService,
new ModuleVulnerabilityCountProviderLS(
this.ossServiceLanguageServer,
languageClientAdapter,
new UriAdapter(),
new TextDocumentAdapter(),
),
this.ossService,
this.ossServiceLanguageServer,
Logger,
new EditorDecorator(vsCodeWindow, vsCodeLanguages, new ThemeColorAdapter()),
new CodeActionKindAdapter(),
this.analytics,
configuration,
);
this.ossVulnerabilityCountService.activate();
this.ossVulnerabilityCountServiceLanguageServer.activate();

this.advisorScoreDisposable = new AdvisorService(
vsCodeWindow,
Expand Down Expand Up @@ -461,7 +461,7 @@ class SnykExtension extends SnykLib implements IExtension {
}

public async deactivate(): Promise<void> {
this.ossVulnerabilityCountService.dispose();
this.ossVulnerabilityCountServiceLanguageServer.dispose();
await this.languageServer.stop();
await this.analytics.flush();
await ErrorReporter.flush();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import marked from 'marked';
import { IAnalytics } from '../../common/analytics/itly';
import { OpenCommandIssueType, OpenIssueCommandArg } from '../../common/commands/types';
import { SNYK_OPEN_ISSUE_COMMAND } from '../../common/constants/commands';
Expand All @@ -16,16 +17,15 @@ import {
} 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';
import { OssVulnerability, isResultCliError } from '../ossResult';
import { ModuleVulnerabilityCountProviderLS } from '../services/vulnerabilityCount/vulnerabilityCountProviderLS';
import { OssIssueCommandArg } from '../views/ossVulnerabilityTreeProvider';

export class VulnerabilityCodeActionProvider implements CodeActionProvider {
export class VulnerabilityCodeActionProviderLS implements CodeActionProvider {
public codeActionKinds: ReadonlyArray<CodeActionKind> = [this.codeActionKindProvider.getQuickFix()];

constructor(
private readonly ossService: OssService,
private readonly vulnerabilityCountProvider: ModuleVulnerabilityCountProvider,
private readonly vulnerabilityCountProvider: ModuleVulnerabilityCountProviderLS,
private readonly codeActionKindProvider: ICodeActionKindAdapter,
private readonly analytics: IAnalytics,
) {}
Expand All @@ -40,7 +40,7 @@ export class VulnerabilityCodeActionProvider implements CodeActionProvider {
return;
}

const ossResult = this.ossService.getResultArray();
const ossResult = this.vulnerabilityCountProvider.getResultArray();
if (!ossResult) {
return;
}
Expand All @@ -65,7 +65,7 @@ export class VulnerabilityCodeActionProvider implements CodeActionProvider {
arguments: [
{
issueType: OpenCommandIssueType.OssVulnerability,
issue: await this.ossService.getOssIssueCommandArg(vulnerability, fileResult.vulnerabilities),
issue: await this.getOssIssueCommandArg(vulnerability, fileResult.vulnerabilities),
} as OpenIssueCommandArg,
],
};
Expand All @@ -78,4 +78,24 @@ export class VulnerabilityCodeActionProvider implements CodeActionProvider {
return [command];
}
}

getOssIssueCommandArg(
vulnerability: OssVulnerability,
allVulnerabilities: OssVulnerability[],
): Promise<OssIssueCommandArg> {
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,
});
});
});
}
}
14 changes: 14 additions & 0 deletions src/snyk/snykOss/ossResult.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import _ from 'lodash';
import { CliError } from '../cli/services/cliService';
import { IssueSeverity } from '../common/languageServer/types';

export type OssResult = OssFileResult[] | OssFileResult;

Expand Down Expand Up @@ -56,3 +57,16 @@ export function capitalizeOssSeverity(ossSeverity: OssSeverity): Capitalize<OssS
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;
}
}
2 changes: 1 addition & 1 deletion src/snyk/snykOss/services/ossService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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 { OssFileResult, OssResult, OssSeverity, OssVulnerability, isResultCliError } from '../ossResult';
import { OssIssueCommandArg } from '../views/ossVulnerabilityTreeProvider';
import { DailyScanJob } from '../watchers/dailyScanJob';
import createManifestFileWatcher from '../watchers/manifestFileWatcher';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import { Diagnostic, DiagnosticCollection, Disposable, TextDocument } from '../.
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 { VulnerabilityCodeActionProviderLS } from '../../codeActions/vulnerabilityCodeActionProviderLS';
import { EditorDecorator } from '../../editor/editorDecorator';
import { VulnerabilityCountHoverProvider } from '../../hoverProvider/vulnerabilityCountHoverProvider';
import { messages } from '../../messages/vulnerabilityCount';
import { OssServiceLanguageServer } from '../../ossServiceLanguageServer';
import { VulnerabilityCountEmitter, VulnerabilityCountEvents } from '../../vulnerabilityCountEmitter';
import { OssService } from '../ossService';
import { ImportedModule, ModuleVulnerabilityCount, ModuleVulnerabilityCountSeverity } from './importedModule';
import { ModuleVulnerabilityCountProvider } from './vulnerabilityCountProvider';
import { ModuleVulnerabilityCountProviderLS } from './vulnerabilityCountProviderLS';

export enum SupportedLanguage {
TypeScript,
Expand All @@ -28,7 +28,7 @@ export enum SupportedLanguage {
PJSON,
}

export class OssVulnerabilityCountService implements Disposable {
export class OssVulnerabilityCountServiceLS implements Disposable {
protected disposables: Disposable[] = [];
protected ossScanFinishedSubscription: Subscription;

Expand All @@ -39,8 +39,8 @@ export class OssVulnerabilityCountService implements Disposable {
private readonly workspace: IVSCodeWorkspace,
private readonly window: IVSCodeWindow,
private readonly languages: IVSCodeLanguages,
private readonly vulnerabilityCountProvider: ModuleVulnerabilityCountProvider,
private readonly ossService: OssService,
private readonly vulnerabilityCountProvider: ModuleVulnerabilityCountProviderLS,
private readonly ossService: OssServiceLanguageServer,
private readonly logger: ILog,
private readonly editorDecorator: EditorDecorator,
private readonly codeActionKindProvider: ICodeActionKindAdapter,
Expand All @@ -67,11 +67,10 @@ export class OssVulnerabilityCountService implements Disposable {
);

// Subscribe to OSS scan finished updates
this.ossScanFinishedSubscription = this.ossService.scanFinished$.subscribe(() => this.processActiveEditor());
this.ossScanFinishedSubscription = this.ossService.newResultAvailable$.subscribe(() => this.processActiveEditor());

[JAVASCRIPT, TYPESCRIPT, PJSON, HTML].forEach(language => {
const provider = new VulnerabilityCodeActionProvider(
this.ossService,
const provider = new VulnerabilityCodeActionProviderLS(
this.vulnerabilityCountProvider,
this.codeActionKindProvider,
this.analytics,
Expand Down Expand Up @@ -188,7 +187,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;
}
Expand Down Expand Up @@ -254,7 +253,7 @@ export class OssVulnerabilityCountService implements Disposable {
],
module,
);
message += messages.decoratorMessage(module.count);

return message;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { CliError } from '../../../cli/services/cliService';
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 { OssFileResult, OssResultBody, OssVulnerability, convertSeverity, isResultCliError } from '../../ossResult';
import { OssServiceLanguageServer } from '../../ossServiceLanguageServer';
import { ImportedModule, ModuleVulnerabilityCount, SeverityCounts } from './importedModule';

export class ModuleVulnerabilityCountProvider {
export class ModuleVulnerabilityCountProviderLS {
constructor(
private readonly ossService: OssService,
private readonly ossService: OssServiceLanguageServer,
private readonly lca: ILanguageClientAdapter,
private readonly uriAdapter: IUriAdapter,
private readonly textDocumentAdapter: ITextDocumentAdapter,
Expand All @@ -29,8 +30,7 @@ export class ModuleVulnerabilityCountProvider {
};

if ([Language.TypeScript, Language.JavaScript, Language.PJSON].includes(language)) {
// TODO use LS when OSS is moved to LS
const ossResult = this.ossService.getResultArray();
const ossResult = this.getResultArray();
if (!ossResult) {
return notCalculated;
}
Expand Down Expand Up @@ -91,7 +91,7 @@ export class ModuleVulnerabilityCountProvider {
continue;
}

const vulnerabilities = this.ossService.getUniqueVulnerabilities((fileResult as OssResultBody).vulnerabilities);
const vulnerabilities = this.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.
Expand Down Expand Up @@ -158,4 +158,73 @@ export class ModuleVulnerabilityCountProvider {
return 0;
})?.[0];
}

public getResultArray = (): ReadonlyArray<OssFileResult> | undefined => {
if (!this.ossService.result) {
return undefined;
}

const tempResultArray: OssFileResult[] = [];
const resultCache = new Map<string, OssResultBody>();

for (const [, value] of this.ossService.result) {
// value is Error
if (value instanceof Error) {
tempResultArray.push(new CliError(value));
}
// value is Issue<T>[]
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: OssVulnerability = {
id: issue.id,
license: issue.additionalData.license,
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,
from: issue.additionalData.from,
upgradePath: issue.additionalData.upgradePath,
isPatchable: issue.additionalData.isPatchable,
isUpgradable: issue.additionalData.isUpgradable,
};
res.vulnerabilities.push(tempVuln);
}
}
}

// copy cached results to final result array
resultCache.forEach(value => tempResultArray.push(value));

return tempResultArray;
};

public getUniqueVulnerabilities(vulnerabilities: OssVulnerability[]): OssVulnerability[] {
return vulnerabilities.filter((val, i, arr) => arr.findIndex(el => el.id === val.id) === i);
}
}
Loading
Loading