From 1b06383f13fecb9961a5d8608976acbf98df400a Mon Sep 17 00:00:00 2001 From: michelkaporin Date: Wed, 6 Apr 2022 16:46:52 +0200 Subject: [PATCH] feat: add advisor preview --- package-lock.json | 44 +++--- package.json | 13 +- src/snyk/advisor/advisorTypes.ts | 15 ++ src/snyk/advisor/editor/editorDecorator.ts | 94 +++++++++++++ src/snyk/advisor/messages/messages.ts | 3 + src/snyk/advisor/services/advisorApiClient.ts | 59 ++++++++ src/snyk/advisor/services/advisorProvider.ts | 48 +++++++ src/snyk/advisor/services/advisorService.ts | 132 ++++++++++++++++++ src/snyk/base/modules/baseSnykModule.ts | 11 +- src/snyk/base/modules/snykLib.ts | 1 + .../common/configuration/configuration.ts | 12 ++ .../constants/languageConsts.ts} | 1 - src/snyk/common/constants/nativeModules.ts | 44 ++++++ src/snyk/common/editor/editorDecorator.ts | 38 +++++ src/snyk/common/parsing.ts | 57 ++++++++ .../common/services/moduleParserProvider.ts | 20 +++ src/snyk/common/types.ts | 24 ++++ src/snyk/common/vscode/markdownString.ts | 12 ++ src/snyk/common/vscode/types.ts | 2 + src/snyk/extension.ts | 19 +++ src/snyk/snykOss/editor/editorDecorator.ts | 49 ++----- .../vulnerabilityCountHoverProvider.ts | 2 +- .../ossVulnerabilityCountService.ts | 75 ++-------- .../vulnerabilityCount/parsers/babelParser.ts | 11 +- .../vulnerabilityCount/parsers/htmlParser.ts | 2 +- .../parsers/moduleParser.ts | 5 +- .../parsers/moduleParserProvider.ts | 19 --- .../parsers/packageJsonParser.ts | 61 +++++--- .../vulnerabilityCountProvider.ts | 8 +- .../advisor/services/advisorProvider.test.ts | 40 ++++++ .../advisor/services/advisorService.test.ts | 105 ++++++++++++++ .../unit/advisor/services/advisorStubs.ts | 40 ++++++ src/test/unit/common/configuration.test.ts | 2 + .../parsers/babelParser.test.ts | 18 +-- .../parsers/moduleParserProvider.test.ts | 9 +- .../parsers/packageJsonParser.test.ts | 3 +- .../vulnerabilityCountProvider.test.ts | 31 ++-- 37 files changed, 911 insertions(+), 218 deletions(-) create mode 100644 src/snyk/advisor/advisorTypes.ts create mode 100644 src/snyk/advisor/editor/editorDecorator.ts create mode 100644 src/snyk/advisor/messages/messages.ts create mode 100644 src/snyk/advisor/services/advisorApiClient.ts create mode 100644 src/snyk/advisor/services/advisorProvider.ts create mode 100644 src/snyk/advisor/services/advisorService.ts rename src/snyk/{snykOss/constants/language.ts => common/constants/languageConsts.ts} (99%) create mode 100644 src/snyk/common/constants/nativeModules.ts create mode 100644 src/snyk/common/editor/editorDecorator.ts create mode 100644 src/snyk/common/parsing.ts create mode 100644 src/snyk/common/services/moduleParserProvider.ts create mode 100644 src/snyk/common/types.ts create mode 100644 src/snyk/common/vscode/markdownString.ts delete mode 100644 src/snyk/snykOss/services/vulnerabilityCount/parsers/moduleParserProvider.ts create mode 100644 src/test/unit/advisor/services/advisorProvider.test.ts create mode 100644 src/test/unit/advisor/services/advisorService.test.ts create mode 100644 src/test/unit/advisor/services/advisorStubs.ts diff --git a/package-lock.json b/package-lock.json index 227f22851..cdaafb4e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12120,11 +12120,6 @@ "chalk": "^4.0.0" } }, - "lru_map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", - "integrity": "sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0=" - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -12134,6 +12129,11 @@ "yallist": "^4.0.0" } }, + "lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0=" + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -13468,23 +13468,6 @@ "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", "dev": true }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } - } - }, "string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", @@ -13521,6 +13504,23 @@ "define-properties": "^1.1.3" } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", diff --git a/package.json b/package.json index 7fa1e89b5..babe990d4 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,13 @@ "reportFalsePositives": { "type": "boolean", "title": "Enable \"report false positives\"", - "description": "Allows reporting false positives for Snyk Code suggestions", + "description": "Allows reporting false positives for Snyk Code suggestions.", + "default": false + }, + "advisor": { + "type": "boolean", + "title": "Enable \"Snyk Advisor\"", + "description": "Discover the health (maintenance, community, popularity & security) status of your open source packages.", "default": false } } @@ -362,8 +368,9 @@ "watch-resources": "sass media --no-source-map --watch", "watch-all": "concurrently --kill-others 'npm run watch' 'npm run watch-resources'", "pretest": "tsc -p ./", - "test:unit": "mocha --ui tdd -c 'out/test/unit/**/*.test.js'", - "test:unit:watch": "mocha --ui tdd -w -c 'out/test/unit/**/*.test.js'", + "test:unit:single": "mocha --ui tdd --require ts-node/register", + "test:unit": "npm run build && mocha --ui tdd -c 'out/test/unit/**/*.test.js'", + "test:unit:watch": "npm run build && mocha --ui tdd -w -c 'out/test/unit/**/*.test.js'", "test:integration": "node ./out/test/integration/runTest.js", "lint": "npx eslint \"src/**/*.ts\"", "lint:fix": "npx eslint --fix \"src/**/*.ts\"", diff --git a/src/snyk/advisor/advisorTypes.ts b/src/snyk/advisor/advisorTypes.ts new file mode 100644 index 000000000..43f9e167f --- /dev/null +++ b/src/snyk/advisor/advisorTypes.ts @@ -0,0 +1,15 @@ +export type AdvisorScoreLabel = { + popularity: string; + maintenance: string; + community: string; + security: string; +}; + +export type AdvisorScore = { + name: string; + score: number; + pending: boolean; + labels: AdvisorScoreLabel; +} | null; + +export type AdvisorRegistry = 'npm-package' | 'python'; diff --git a/src/snyk/advisor/editor/editorDecorator.ts b/src/snyk/advisor/editor/editorDecorator.ts new file mode 100644 index 000000000..0699f5a9e --- /dev/null +++ b/src/snyk/advisor/editor/editorDecorator.ts @@ -0,0 +1,94 @@ +import { getRenderOptions, LineDecorations, updateDecorations } from '../../common/editor/editorDecorator'; +import { HoverAdapter } from '../../common/vscode/hover'; +import { IVSCodeLanguages } from '../../common/vscode/languages'; +import { IMarkdownStringAdapter } from '../../common/vscode/markdownString'; +import { IThemeColorAdapter } from '../../common/vscode/theme'; +import { Hover, TextEditorDecorationType } from '../../common/vscode/types'; +import { IVSCodeWindow } from '../../common/vscode/window'; +import { AdvisorScore } from '../advisorTypes'; +import { messages } from '../messages/messages'; +import { IAdvisorApiClient } from '../services/advisorApiClient'; + +const { SCORE_PREFIX } = messages; + +export default class EditorDecorator { + private readonly decorationType: TextEditorDecorationType; + private readonly editorLastCharacterIndex = Number.MAX_SAFE_INTEGER; + private readonly fileDecorationLines: Map = new Map(); + + constructor( + private readonly window: IVSCodeWindow, + private readonly languages: IVSCodeLanguages, + private readonly themeColorAdapter: IThemeColorAdapter, + private readonly advisorApiClient: IAdvisorApiClient, + private readonly hoverAdapter: HoverAdapter, + private readonly markdownStringAdapter: IMarkdownStringAdapter, + ) { + this.decorationType = this.window.createTextEditorDecorationType({ + after: { margin: '0 0 0 1rem' }, + }); + } + + addScoresDecorations( + filePath: string, + packageScore: AdvisorScore, + line: number, + decorations: LineDecorations = [], + ): void { + if (!packageScore) { + return; + } + decorations[line] = { + range: this.languages.createRange( + line - 1, + this.editorLastCharacterIndex, + line - 1, + this.editorLastCharacterIndex, + ), + renderOptions: getRenderOptions( + `${SCORE_PREFIX} ${Math.round(packageScore.score * 100)}/100`, + this.themeColorAdapter, + ), + hoverMessage: this.getHoverMessage(packageScore)?.contents, + }; + + this.fileDecorationLines.set(filePath, decorations); + updateDecorations(this.window, filePath, decorations, this.decorationType); + } + + getHoverMessage(score: AdvisorScore): Hover | null { + if (!score) { + return null; + } + const hoverMessageMarkdown = this.markdownStringAdapter.get(``); + hoverMessageMarkdown.isTrusted = true; + const hoverMessage = this.hoverAdapter.create(hoverMessageMarkdown); + hoverMessageMarkdown.appendMarkdown('| | | | |'); + hoverMessageMarkdown.appendMarkdown('\n'); + hoverMessageMarkdown.appendMarkdown('| ---- | ---- | ---- | :---- |'); + hoverMessageMarkdown.appendMarkdown('\n'); + Object.keys(score.labels).forEach(label => { + hoverMessageMarkdown.appendMarkdown(`| ${label}: | | | ${score?.labels[label]} |`); + hoverMessageMarkdown.appendMarkdown('\n'); + }); + hoverMessageMarkdown.appendMarkdown( + `[More Details](${this.advisorApiClient.getAdvisorUrl('npm-package')}/${score.name})`, + ); + + return hoverMessage; + } + + resetDecorations(filePath: string): void { + const decorations: LineDecorations | undefined = this.fileDecorationLines.get(filePath); + if (!decorations) { + return; + } + + const emptyDecorations = decorations.map(d => ({ + ...d, + renderOptions: getRenderOptions('', this.themeColorAdapter), + })); + + updateDecorations(this.window, filePath, emptyDecorations, this.decorationType); + } +} diff --git a/src/snyk/advisor/messages/messages.ts b/src/snyk/advisor/messages/messages.ts new file mode 100644 index 000000000..d05345c16 --- /dev/null +++ b/src/snyk/advisor/messages/messages.ts @@ -0,0 +1,3 @@ +export const messages = { + SCORE_PREFIX: 'Advisor Score', +}; diff --git a/src/snyk/advisor/services/advisorApiClient.ts b/src/snyk/advisor/services/advisorApiClient.ts new file mode 100644 index 000000000..9e3e13236 --- /dev/null +++ b/src/snyk/advisor/services/advisorApiClient.ts @@ -0,0 +1,59 @@ +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import { DEFAULT_API_HEADERS } from '../../common/api/headers'; +import { IConfiguration } from '../../common/configuration/configuration'; +import { ILog } from '../../common/logger/interfaces'; +import { AdvisorRegistry } from '../advisorTypes'; + +export interface IAdvisorApiClient { + post>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise; + apiPath: string; + getAdvisorUrl(registry: AdvisorRegistry): string; +} + +export class AdvisorApiClient implements IAdvisorApiClient { + private instance: AxiosInstance | null = null; + private readonly advisorBaseUrl = 'https://snyk.io/advisor'; + apiPath = `/unstable/advisor/scores/npm-package`; + + constructor(private readonly configuration: IConfiguration, private readonly logger: ILog) {} + + getAdvisorUrl(registry: AdvisorRegistry): string { + return `${this.advisorBaseUrl}/${registry}`; + } + + private get http(): AxiosInstance { + return this.instance != null ? this.instance : this.initHttp(); + } + + initHttp(): AxiosInstance { + const http = axios.create({ + headers: DEFAULT_API_HEADERS, + responseType: 'json', + }); + + http.interceptors.response.use( + response => response, + (error: Error) => { + this.logger.error(`Call to Advisor API failed: ${error.message}`); + return Promise.reject(error); + }, + ); + + this.instance = http; + return http; + } + + async post>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { + const token = await this.configuration.getToken(); + this.http.interceptors.request.use(req => { + req.baseURL = this.configuration.baseApiUrl; + req.headers = { + ...req.headers, + Authorization: `token ${token}`, + } as { [header: string]: string }; + + return req; + }); + return this.http.post(url, data, config); + } +} diff --git a/src/snyk/advisor/services/advisorProvider.ts b/src/snyk/advisor/services/advisorProvider.ts new file mode 100644 index 000000000..4d3efb835 --- /dev/null +++ b/src/snyk/advisor/services/advisorProvider.ts @@ -0,0 +1,48 @@ +import { AxiosResponse } from 'axios'; +import { ILog } from '../../common/logger/interfaces'; +import { ImportedModule } from '../../common/types'; +import { AdvisorScore } from '../advisorTypes'; +import { IAdvisorApiClient } from './advisorApiClient'; + +export default class AdvisorProvider { + protected scores: AdvisorScore[]; + private cachePackages: string[] = []; + + constructor(private readonly advisorApiClient: IAdvisorApiClient, private readonly logger: ILog) {} + + public async getScores(modules: ImportedModule[]): Promise { + const scores: AdvisorScore[] = []; + try { + const packages = modules.map(({ name }) => name); + if (!packages.filter(pkg => !this.cachePackages.includes(pkg)).length) { + return this.scores; + } + if (!packages.length) { + return scores; + } + const res: AxiosResponse = await this.advisorApiClient.post( + this.advisorApiClient.apiPath, + modules.map(({ name }) => name), + ); + + if (res.data) { + this.scores = res.data as AdvisorScore[]; + this.cachePackages = this.scores.map(advisorScore => { + if (!advisorScore) { + return ''; + } + if (!advisorScore.name) { + return ''; + } + return advisorScore.name; + }); + return res.data as AdvisorScore[]; + } + } catch (err) { + if (err instanceof Error) { + this.logger.error(`Failed to get scores: ${err.message}`); + } + } + return scores; + } +} diff --git a/src/snyk/advisor/services/advisorService.ts b/src/snyk/advisor/services/advisorService.ts new file mode 100644 index 000000000..e0fe2f41b --- /dev/null +++ b/src/snyk/advisor/services/advisorService.ts @@ -0,0 +1,132 @@ +import { Subscription } from 'rxjs'; +import { IConfiguration } from '../../common/configuration/configuration'; +import { LineDecorations } from '../../common/editor/editorDecorator'; +import { ILog } from '../../common/logger/interfaces'; +import { getSupportedLanguage, isValidModuleName } from '../../common/parsing'; +import { ModuleParserProvider } from '../../common/services/moduleParserProvider'; +import { ImportedModule, Language } from '../../common/types'; +import { HoverAdapter } from '../../common/vscode/hover'; +import { IVSCodeLanguages } from '../../common/vscode/languages'; +import { IMarkdownStringAdapter } from '../../common/vscode/markdownString'; +import { IThemeColorAdapter } from '../../common/vscode/theme'; +import { Disposable, TextDocument, TextEditor } from '../../common/vscode/types'; +import { IVSCodeWindow } from '../../common/vscode/window'; +import { IVSCodeWorkspace } from '../../common/vscode/workspace'; +import { AdvisorScore } from '../advisorTypes'; +import EditorDecorator from '../editor/editorDecorator'; +import { IAdvisorApiClient } from './advisorApiClient'; +import AdvisorProvider from './advisorProvider'; + +const SCORE_THRESHOLD = 0.7; +export class AdvisorService implements Disposable { + protected disposables: Disposable[] = []; + protected advisorScanFinishedSubscription: Subscription; + protected activeEditor: TextEditor | undefined; + + private readonly editorDecorator: EditorDecorator; + + constructor( + private readonly window: IVSCodeWindow, + private readonly languages: IVSCodeLanguages, + private readonly advisorProvider: AdvisorProvider, + private readonly logger: ILog, + private readonly workspace: IVSCodeWorkspace, + private readonly advisorApiClient: IAdvisorApiClient, + private readonly themeColorAdapter: IThemeColorAdapter, + private readonly hoverAdapter: HoverAdapter, + private readonly markdownStringAdapter: IMarkdownStringAdapter, + private readonly configuration: IConfiguration, + ) { + this.editorDecorator = new EditorDecorator( + window, + this.languages, + this.themeColorAdapter, + this.advisorApiClient, + this.hoverAdapter, + this.markdownStringAdapter, + ); + } + + async activate(): Promise { + if (!this.configuration.getPreviewFeatures().advisor) { + return; + } + + this.activeEditor = this.window.getActiveTextEditor(); + this.registerEditorListeners(); + if (!this.activeEditor) { + return; + } + + await this.handleEditorEvent(this.activeEditor.document); + } + + registerEditorListeners(): void { + this.disposables.push( + this.workspace.onDidChangeTextDocument(async ev => { + if (ev?.contentChanges.length) { + this.editorDecorator.resetDecorations(ev.document.fileName); + } + await this.handleEditorEvent(ev.document); + }), + this.window.onDidChangeActiveTextEditor(async ev => { + if (!ev) { + return; + } + await this.handleEditorEvent(ev.document); + }), + ); + } + + async handleEditorEvent(document: TextDocument): Promise { + const { fileName, languageId } = document; + const supportedLanguage = getSupportedLanguage(fileName, languageId); + if (document.isDirty || !supportedLanguage) { + return; + } + + const modules = this.getModules(fileName, document.getText(), supportedLanguage, this.logger).filter( + isValidModuleName, + ); + + const scores = await this.advisorProvider.getScores(modules); + this.processScores(scores, modules, fileName); + } + + processScores(scores: AdvisorScore[], modules: ImportedModule[], fileName: string): void { + const vulnsLineDecorations: Map = new Map(); + modules.forEach(({ name, line }) => { + vulnsLineDecorations.set(name, line || -1); + }); + const decorations: LineDecorations = []; + for (const [packageName, line] of vulnsLineDecorations) { + if (line < 0) { + continue; + } + const packageScore = scores.find(score => score && score.name === packageName); + if (!packageScore || packageScore.score >= SCORE_THRESHOLD) { + continue; + } + + this.editorDecorator.addScoresDecorations(fileName, packageScore, line, decorations); + } + } + + private getModules(fileName: string, source: string, language: Language, logger: ILog): ImportedModule[] { + const parser = ModuleParserProvider.getInstance(language, logger); + if (!parser) { + return []; + } + + return parser.getModules(fileName, source, language); + } + + dispose(): void { + while (this.disposables.length) { + const disposable = this.disposables.pop(); + if (disposable) { + disposable.dispose(); + } + } + } +} diff --git a/src/snyk/base/modules/baseSnykModule.ts b/src/snyk/base/modules/baseSnykModule.ts index 78cece830..bbc45e8a0 100644 --- a/src/snyk/base/modules/baseSnykModule.ts +++ b/src/snyk/base/modules/baseSnykModule.ts @@ -1,9 +1,11 @@ +import { AdvisorApiClient, IAdvisorApiClient } from '../../advisor/services/advisorApiClient'; +import AdvisorProvider from '../../advisor/services/advisorProvider'; +import { AdvisorService } from '../../advisor/services/advisorService'; import { CliDownloadService } from '../../cli/services/cliDownloadService'; import { IAnalytics } from '../../common/analytics/itly'; import { ISnykApiClient, SnykApiClient } from '../../common/api/apiСlient'; import { CommandController } from '../../common/commands/commandController'; import { configuration } from '../../common/configuration/instance'; -import { ISnykCodeErrorHandler, SnykCodeErrorHandler } from '../../snykCode/error/snykCodeErrorHandler'; import { ExperimentService } from '../../common/experiment/services/experimentService'; import { Logger } from '../../common/logger/logger'; import { ContextService, IContextService } from '../../common/services/contextService'; @@ -12,9 +14,11 @@ import { IOpenerService, OpenerService } from '../../common/services/openerServi import { IViewManagerService, ViewManagerService } from '../../common/services/viewManagerService'; import { User } from '../../common/user'; import { ExtensionContext } from '../../common/vscode/extensionContext'; +import { vsCodeWorkspace } from '../../common/vscode/workspace'; import { IWatcher } from '../../common/watchers/interfaces'; import { ISnykCodeService } from '../../snykCode/codeService'; import { CodeSettings, ICodeSettings } from '../../snykCode/codeSettings'; +import { ISnykCodeErrorHandler, SnykCodeErrorHandler } from '../../snykCode/error/snykCodeErrorHandler'; import { FalsePositiveApi, IFalsePositiveApi } from '../../snykCode/falsePositive/api/falsePositiveApi'; import SnykEditorsWatcher from '../../snykCode/watchers/editorsWatcher'; import { OssService } from '../../snykOss/services/ossService'; @@ -24,7 +28,6 @@ import { ScanModeService } from '../services/scanModeService'; import SnykStatusBarItem, { IStatusBarItem } from '../statusBarItem/statusBarItem'; import { ILoadingBadge, LoadingBadge } from '../views/loadingBadge'; import { IBaseSnykModule } from './interfaces'; -import { vsCodeWorkspace } from '../../common/vscode/workspace'; export default abstract class BaseSnykModule implements IBaseSnykModule { context: ExtensionContext; @@ -40,14 +43,17 @@ export default abstract class BaseSnykModule implements IBaseSnykModule { protected authService: IAuthenticationService; protected cliDownloadService: CliDownloadService; protected ossService?: OssService; + protected advisorService?: AdvisorProvider; protected commandController: CommandController; protected scanModeService: ScanModeService; protected ossVulnerabilityCountService: OssVulnerabilityCountService; + protected advisorScoreDisposable: AdvisorService; protected notificationService: INotificationService; protected analytics: IAnalytics; protected snykApiClient: ISnykApiClient; + protected advisorApiClient: IAdvisorApiClient; protected falsePositiveApi: IFalsePositiveApi; snykCode: ISnykCodeService; protected codeSettings: ICodeSettings; @@ -75,6 +81,7 @@ export default abstract class BaseSnykModule implements IBaseSnykModule { configuration, ); this.codeSettings = new CodeSettings(this.snykApiClient, this.contextService, configuration, this.openerService); + this.advisorApiClient = new AdvisorApiClient(configuration, Logger); } abstract runScan(): Promise; diff --git a/src/snyk/base/modules/snykLib.ts b/src/snyk/base/modules/snykLib.ts index 0e8ba6a82..4c8c3e7d1 100644 --- a/src/snyk/base/modules/snykLib.ts +++ b/src/snyk/base/modules/snykLib.ts @@ -132,6 +132,7 @@ export default class SnykLib extends BaseSnykModule implements ISnykLib { try { const oldResult = this.ossService.getResult(); const result = await this.ossService.test(manual, reportTriggeredEvent); + if (result instanceof CliError) { return; } diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index add31b683..eec0e6aba 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -39,6 +39,7 @@ export interface SeverityFilter { export type PreviewFeatures = { reportFalsePositives: boolean | undefined; + advisor: boolean | undefined; }; export interface IConfiguration { @@ -46,6 +47,7 @@ export interface IConfiguration { source: string; authHost: string; + baseApiUrl: string; getToken(): Promise; setToken(token: string): Promise; clearToken(): Promise; @@ -80,6 +82,8 @@ export class Configuration implements IConfiguration { private readonly defaultSnykCodeBaseURL = 'https://deeproxy.snyk.io'; private readonly defaultAuthHost = 'https://snyk.io'; private readonly defaultOssApiEndpoint = `${this.defaultAuthHost}/api/v1`; + private readonly defaultBaseApiHost = 'https://api.snyk.io'; + private readonly devBaseApiHost = 'https://api.dev.snyk.io'; constructor(private processEnv: NodeJS.ProcessEnv = process.env, private workspace: IVSCodeWorkspace) {} @@ -180,6 +184,13 @@ export class Configuration implements IConfiguration { return Configuration.source; } + get baseApiUrl(): string { + if (this.isDevelopment) { + return this.devBaseApiHost; + } + return this.defaultBaseApiHost; + } + getFeaturesConfiguration(): FeaturesConfiguration | undefined { const ossEnabled = this.workspace.getConfiguration( CONFIGURATION_IDENTIFIER, @@ -313,6 +324,7 @@ export class Configuration implements IConfiguration { getPreviewFeatures(): PreviewFeatures { const defaulSetting: PreviewFeatures = { reportFalsePositives: false, + advisor: false, }; const userSetting = diff --git a/src/snyk/snykOss/constants/language.ts b/src/snyk/common/constants/languageConsts.ts similarity index 99% rename from src/snyk/snykOss/constants/language.ts rename to src/snyk/common/constants/languageConsts.ts index a62c67f4b..45d5b3ea9 100644 --- a/src/snyk/snykOss/constants/language.ts +++ b/src/snyk/common/constants/languageConsts.ts @@ -1,7 +1,6 @@ export const TYPESCRIPT_FILE_REGEX = new RegExp('\\.tsx?$'); export const JAVASCRIPT_FILE_REGEX = new RegExp('\\.jsx?$'); export const HTML_FILE_REGEX = new RegExp('\\.html?$'); - export const TYPESCRIPT = 'typescript'; export const TYPESCRIPT_REACT = 'typescriptreact'; export const JAVASCRIPT = 'javascript'; diff --git a/src/snyk/common/constants/nativeModules.ts b/src/snyk/common/constants/nativeModules.ts new file mode 100644 index 000000000..ec5d988e7 --- /dev/null +++ b/src/snyk/common/constants/nativeModules.ts @@ -0,0 +1,44 @@ +export default [ + 'assert', + 'async_hooks', + 'buffer', + 'child_process', + 'cluster', + 'console', + 'constants', + 'crypto', + 'dgram', + 'dns', + 'domain', + 'events', + 'fs', + 'http', + 'http2', + 'https', + 'inspector', + 'internal', + 'module', + 'net', + 'os', + 'path', + 'perf_hooks', + 'process', + 'punycode', + 'querystring', + 'readline', + 'repl', + 'stream', + 'string_decoder', + 'sys', + 'timers', + 'tls', + 'trace_events', + 'tty', + 'url', + 'util', + 'v8', + 'vm', + 'wasi', + 'worker_threads', + 'zlib', +]; diff --git a/src/snyk/common/editor/editorDecorator.ts b/src/snyk/common/editor/editorDecorator.ts new file mode 100644 index 000000000..432da67af --- /dev/null +++ b/src/snyk/common/editor/editorDecorator.ts @@ -0,0 +1,38 @@ +import { IThemeColorAdapter } from '../vscode/theme'; +import { DecorationOptions, TextEditorDecorationType, ThemableDecorationInstanceRenderOptions } from '../vscode/types'; +import { IVSCodeWindow } from '../vscode/window'; + +export type LineDecorations = DecorationOptions[]; + +export function updateDecorations( + window: IVSCodeWindow, + filePath: string, + decorations: LineDecorations, + decorationType: TextEditorDecorationType, +): void { + const visibleEditors = window.getVisibleTextEditors().filter(editor => editor.document.fileName === filePath); + for (const editor of visibleEditors) { + if (decorations && decorations.length) { + editor.setDecorations( + decorationType, + decorations.filter(d => !!d).filter(({ renderOptions }) => renderOptions?.after?.contentText), + ); + } + } +} + +export function getRenderOptions( + contentText: string, + themeColorAdapter: IThemeColorAdapter, +): ThemableDecorationInstanceRenderOptions { + const color = themeColorAdapter.create('descriptionForeground'); + const fontWeight = 'normal'; + + return { + after: { + contentText, + color, + fontWeight, + }, + }; +} diff --git a/src/snyk/common/parsing.ts b/src/snyk/common/parsing.ts new file mode 100644 index 000000000..bd0c77ee0 --- /dev/null +++ b/src/snyk/common/parsing.ts @@ -0,0 +1,57 @@ +import npmValidPackageName from 'validate-npm-package-name'; +import { + HTML, + HTML_FILE_REGEX, + JAVASCRIPT, + JAVASCRIPT_FILE_REGEX, + JAVASCRIPT_REACT, + PJSON, + TYPESCRIPT, + TYPESCRIPT_FILE_REGEX, + TYPESCRIPT_REACT, +} from './constants/languageConsts'; +import nativeModules from './constants/nativeModules'; +import { ImportedModule, Language } from './types'; + +export function getSupportedLanguage(fileName: string, languageId: string): Language | null { + if (languageId === TYPESCRIPT || languageId === TYPESCRIPT_REACT || TYPESCRIPT_FILE_REGEX.test(fileName)) { + return Language.TypeScript; + } else if (languageId === JAVASCRIPT || languageId === JAVASCRIPT_REACT || JAVASCRIPT_FILE_REGEX.test(fileName)) { + return Language.JavaScript; + } else if (languageId === HTML || HTML_FILE_REGEX.test(fileName)) { + return Language.HTML; + } else if (languageId === PJSON && fileName.endsWith('package.json')) { + return Language.PJSON; + } + + return null; +} + +export function isValidModuleName(module: ImportedModule): boolean { + const moduleName = module.name; + if (nativeModules.includes(moduleName.toLowerCase())) { + return false; + } + + if (moduleName.trim() == '' || /^[.~]/.test(moduleName)) { + return false; + } + + if (moduleName.includes('/') && !moduleName.startsWith('@')) { + const newName = module.name.split('/').shift(); + if (newName) { + // mutating… + module.name = newName; + } else { + return false; + } + } + + const valid = npmValidPackageName(module.name); + if (valid.errors) { + // invalid package name, so isn't real, so we'll bail + return false; + } + + return true; +} diff --git a/src/snyk/common/services/moduleParserProvider.ts b/src/snyk/common/services/moduleParserProvider.ts new file mode 100644 index 000000000..94c423911 --- /dev/null +++ b/src/snyk/common/services/moduleParserProvider.ts @@ -0,0 +1,20 @@ +import { BabelParser } from '../../snykOss/services/vulnerabilityCount/parsers/babelParser'; +import { HtmlParser } from '../../snykOss/services/vulnerabilityCount/parsers/htmlParser'; +import { ModuleParser } from '../../snykOss/services/vulnerabilityCount/parsers/moduleParser'; +import { PackageJsonParser } from '../../snykOss/services/vulnerabilityCount/parsers/packageJsonParser'; +import { ILog } from '../logger/interfaces'; +import { Language } from '../types'; + +export class ModuleParserProvider { + static getInstance(language: Language, logger: ILog): ModuleParser | undefined { + if ([Language.TypeScript, Language.JavaScript].includes(language)) { + return new BabelParser(); + } else if (language === Language.PJSON) { + return new PackageJsonParser(logger); + } else if (language === Language.HTML) { + return new HtmlParser(); + } + + return undefined; + } +} diff --git a/src/snyk/common/types.ts b/src/snyk/common/types.ts new file mode 100644 index 000000000..6b3fd99e6 --- /dev/null +++ b/src/snyk/common/types.ts @@ -0,0 +1,24 @@ +export enum Language { + TypeScript, + JavaScript, + HTML, + PJSON, +} +export type OssRange = { + start: { + line: number; + column: number; + }; + end: { + line: number; + column: number; + }; +}; +export type ImportedModule = { + fileName: string; + name: string; + line: number | null; + loc: OssRange | null; + string: string; + version?: string; +}; diff --git a/src/snyk/common/vscode/markdownString.ts b/src/snyk/common/vscode/markdownString.ts new file mode 100644 index 000000000..ea53f74bd --- /dev/null +++ b/src/snyk/common/vscode/markdownString.ts @@ -0,0 +1,12 @@ +import * as vscode from 'vscode'; +import { MarkdownString } from './types'; + +export interface IMarkdownStringAdapter { + get(value?: string, supportThemeIcons?: boolean): MarkdownString; +} + +export class MarkdownStringAdapter implements IMarkdownStringAdapter { + get(value?: string, supportThemeIcons?: boolean): vscode.MarkdownString { + return new vscode.MarkdownString(value, supportThemeIcons); + } +} diff --git a/src/snyk/common/vscode/types.ts b/src/snyk/common/vscode/types.ts index 8474b6074..8cc0165f5 100644 --- a/src/snyk/common/vscode/types.ts +++ b/src/snyk/common/vscode/types.ts @@ -26,6 +26,7 @@ export type SecretStorageChangeEvent = vscode.SecretStorageChangeEvent; export type Event = vscode.Event; export type Uri = vscode.Uri; export type MarkedString = vscode.MarkedString; +export type MarkdownString = vscode.MarkdownString; export type Hover = vscode.Hover; export type CodeAction = vscode.CodeAction; export type CodeActionKind = vscode.CodeActionKind; @@ -39,4 +40,5 @@ export type ThemableDecorationInstanceRenderOptions = vscode.ThemableDecorationI export type CodeActionProviderMetadata = vscode.CodeActionProviderMetadata; export type ExtensionContext = vscode.ExtensionContext; export type WebviewOptions = vscode.WebviewOptions; +export type TextDocumentChangeEvent = vscode.TextDocumentChangeEvent; export type InputBoxOptions = vscode.InputBoxOptions; diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index 310f1f0a0..57331a3ed 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -1,4 +1,6 @@ import * as vscode from 'vscode'; +import AdvisorProvider from './advisor/services/advisorProvider'; +import { AdvisorService } from './advisor/services/advisorService'; import { IExtension } from './base/modules/interfaces'; import SnykLib from './base/modules/snykLib'; import { AuthenticationService } from './base/services/authenticationService'; @@ -49,7 +51,9 @@ import { CodeActionKindAdapter } from './common/vscode/codeAction'; import { vsCodeComands } from './common/vscode/commands'; import { vsCodeEnv } from './common/vscode/env'; import { extensionContext } from './common/vscode/extensionContext'; +import { HoverAdapter } from './common/vscode/hover'; import { vsCodeLanguages, VSCodeLanguages } from './common/vscode/languages'; +import { MarkdownStringAdapter } from './common/vscode/markdownString'; import SecretStorageAdapter from './common/vscode/secretStorage'; import { ThemeColorAdapter } from './common/vscode/theme'; import { Range, Uri } from './common/vscode/types'; @@ -151,6 +155,7 @@ class SnykExtension extends SnykLib implements IExtension { new UriAdapter(), ); + this.advisorService = new AdvisorProvider(this.advisorApiClient, Logger); this.cliDownloadService = new CliDownloadService( this.context, new StaticCliApi(vsCodeWorkspace), @@ -281,6 +286,20 @@ class SnykExtension extends SnykLib implements IExtension { ); this.ossVulnerabilityCountService.activate(); + this.advisorScoreDisposable = new AdvisorService( + vsCodeWindow, + vsCodeLanguages, + this.advisorService, + Logger, + vsCodeWorkspace, + this.advisorApiClient, + new ThemeColorAdapter(), + new HoverAdapter(), + new MarkdownStringAdapter(), + configuration, + ); + void this.advisorScoreDisposable.activate(); + // Actually start analysis this.runScan(); } diff --git a/src/snyk/snykOss/editor/editorDecorator.ts b/src/snyk/snykOss/editor/editorDecorator.ts index 0f1d12e22..398a5d888 100644 --- a/src/snyk/snykOss/editor/editorDecorator.ts +++ b/src/snyk/snykOss/editor/editorDecorator.ts @@ -1,17 +1,12 @@ import _ from 'lodash'; +import { getRenderOptions, LineDecorations, updateDecorations } from '../../common/editor/editorDecorator'; import { IVSCodeLanguages } from '../../common/vscode/languages'; import { IThemeColorAdapter } from '../../common/vscode/theme'; -import { - DecorationOptions, - TextEditorDecorationType, - ThemableDecorationInstanceRenderOptions, -} from '../../common/vscode/types'; +import { TextEditorDecorationType } from '../../common/vscode/types'; import { IVSCodeWindow } from '../../common/vscode/window'; import { messages } from '../messages/vulnerabilityCount'; import { ImportedModule, ModuleVulnerabilityCount } from '../services/vulnerabilityCount/importedModule'; -type LineDecorations = DecorationOptions[]; // array index is a line number - export class EditorDecorator { private readonly decorationType: TextEditorDecorationType; private readonly fileDecorationMap: Map; @@ -42,7 +37,7 @@ export class EditorDecorator { const emptyDecorations = decorations.map(d => ({ ...d, - renderOptions: this.getRenderOptions(''), + renderOptions: getRenderOptions('', this.themeColorAdapter), })); this.fileDecorationMap.set(filePath, emptyDecorations); this.triggerUpdateDecorations(filePath); @@ -63,7 +58,7 @@ export class EditorDecorator { module.line - 1, this.editorLastCharacterIndex, ), - renderOptions: this.getRenderOptions(messages.fetchingVulnerabilities), + renderOptions: getRenderOptions(messages.fetchingVulnerabilities, this.themeColorAdapter), }; } @@ -106,7 +101,7 @@ export class EditorDecorator { vulnerabilityCount.line - 1, this.editorLastCharacterIndex, ), - renderOptions: this.getRenderOptions(text), + renderOptions: getRenderOptions(text, this.themeColorAdapter), }; if (triggerUpdate) { @@ -114,40 +109,16 @@ export class EditorDecorator { } } - updateDecorations(filePath: string): void { - const visibleEditors = this.window.getVisibleTextEditors().filter(editor => editor.document.fileName === filePath); - - for (const editor of visibleEditors) { - const decorations = this.fileDecorationMap.get(filePath); - - if (decorations && decorations.length) { - editor.setDecorations( - this.decorationType, - decorations.filter(d => !!d), - ); - } - } - } - private triggerUpdateDecorations(filePath: string, updateTimeoutInMs = 10): void { if (this.updateTimeout) { clearTimeout(this.updateTimeout); this.updateTimeout = undefined; } - this.updateTimeout = setTimeout(() => this.updateDecorations(filePath), updateTimeoutInMs); - } - - private getRenderOptions(contentText: string): ThemableDecorationInstanceRenderOptions { - const color = this.themeColorAdapter.create('descriptionForeground'); - const fontWeight = 'normal'; - - return { - after: { - contentText, - color, - fontWeight, - }, - }; + const lineDecorations = this.fileDecorationMap.get(filePath) || []; + this.updateTimeout = setTimeout( + () => updateDecorations(this.window, filePath, lineDecorations, this.decorationType), + updateTimeoutInMs, + ); } } diff --git a/src/snyk/snykOss/hoverProvider/vulnerabilityCountHoverProvider.ts b/src/snyk/snykOss/hoverProvider/vulnerabilityCountHoverProvider.ts index f1cfc9d63..8fc56f11d 100644 --- a/src/snyk/snykOss/hoverProvider/vulnerabilityCountHoverProvider.ts +++ b/src/snyk/snykOss/hoverProvider/vulnerabilityCountHoverProvider.ts @@ -1,9 +1,9 @@ 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'; -import { SupportedLanguageIds } from '../constants/language'; export class VulnerabilityCountHoverProvider implements Disposable { private hoverProvider: Disposable | undefined; diff --git a/src/snyk/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService.ts b/src/snyk/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService.ts index 229814c82..4ca5ad84b 100644 --- a/src/snyk/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService.ts +++ b/src/snyk/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService.ts @@ -1,7 +1,10 @@ import { Subscription } from 'rxjs'; -import npmValidPackageName from 'validate-npm-package-name'; import { IAnalytics } from '../../../common/analytics/itly'; +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'; @@ -9,25 +12,12 @@ 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 { - HTML, - HTML_FILE_REGEX, - JAVASCRIPT, - JAVASCRIPT_FILE_REGEX, - JAVASCRIPT_REACT, - PJSON, - TYPESCRIPT, - TYPESCRIPT_FILE_REGEX, - TYPESCRIPT_REACT, -} from '../../constants/language'; -import nativeModules from '../../constants/nativeModules'; 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 { ImportedModule, ModuleVulnerabilityCount, ModuleVulnerabilityCountSeverity } from './importedModule'; -import { ModuleParserProvider } from './parsers/moduleParserProvider'; import { ModuleVulnerabilityCountProvider } from './vulnerabilityCountProvider'; export enum SupportedLanguage { @@ -124,7 +114,7 @@ export class OssVulnerabilityCountService implements Disposable { } const { fileName, languageId } = document; - const supportedLanguage = this.getSupportedLanguage(fileName, languageId); + const supportedLanguage = getSupportedLanguage(fileName, languageId); if (supportedLanguage === null || !this.shouldProcessFile(fileName, supportedLanguage)) { return false; } @@ -193,8 +183,8 @@ export class OssVulnerabilityCountService implements Disposable { this.diagnostics.set(document.uri, diagnostics); } - private shouldProcessFile(fileName: string, language: SupportedLanguage): boolean { - if ([SupportedLanguage.TypeScript, SupportedLanguage.JavaScript, SupportedLanguage.PJSON].includes(language)) { + private shouldProcessFile(fileName: string, language: Language): boolean { + if ([Language.TypeScript, Language.JavaScript, Language.PJSON].includes(language)) { const ossResult = this.ossService.getResultArray(); if (!ossResult) { return false; @@ -212,28 +202,14 @@ export class OssVulnerabilityCountService implements Disposable { return true; } - private getSupportedLanguage(fileName: string, languageId: string): SupportedLanguage | null { - if (languageId === TYPESCRIPT || languageId === TYPESCRIPT_REACT || TYPESCRIPT_FILE_REGEX.test(fileName)) { - return SupportedLanguage.TypeScript; - } else if (languageId === JAVASCRIPT || languageId === JAVASCRIPT_REACT || JAVASCRIPT_FILE_REGEX.test(fileName)) { - return SupportedLanguage.JavaScript; - } else if (languageId === HTML || HTML_FILE_REGEX.test(fileName)) { - return SupportedLanguage.HTML; - } else if (languageId === PJSON && fileName.endsWith('package.json')) { - return SupportedLanguage.PJSON; - } - - return null; - } - private async getImportedModules( fileName: string, content: string, - language: SupportedLanguage, + language: Language, emitter: VulnerabilityCountEmitter, ): Promise { try { - const modules = this.getModules(fileName, content, language).filter(module => this.isValidModuleName(module)); + const modules = this.getModules(fileName, content, language).filter(isValidModuleName); emitter.startScanning(modules); const promises = modules @@ -251,37 +227,8 @@ export class OssVulnerabilityCountService implements Disposable { } } - private isValidModuleName(module: ImportedModule): boolean { - const moduleName = module.name; - if (nativeModules.includes(moduleName.toLowerCase())) { - return false; - } - - if (moduleName.trim() == '' || /^[.~]/.test(moduleName)) { - return false; - } - - if (moduleName.includes('/') && !moduleName.startsWith('@')) { - const newName = module.name.split('/').shift(); - if (newName) { - // mutating… - module.name = newName; - } else { - return false; - } - } - - const valid = npmValidPackageName(module.name); - if (valid.errors) { - // invalid package name, so isn't real, so we'll bail - return false; - } - - return true; - } - - private getModules(fileName: string, source: string, language: SupportedLanguage): ImportedModule[] { - const parser = ModuleParserProvider.getInstance(language); + private getModules(fileName: string, source: string, language: Language): ImportedModule[] { + const parser = ModuleParserProvider.getInstance(language, this.logger); if (!parser) { return []; } diff --git a/src/snyk/snykOss/services/vulnerabilityCount/parsers/babelParser.ts b/src/snyk/snykOss/services/vulnerabilityCount/parsers/babelParser.ts index 888853254..c3ca96d01 100644 --- a/src/snyk/snykOss/services/vulnerabilityCount/parsers/babelParser.ts +++ b/src/snyk/snykOss/services/vulnerabilityCount/parsers/babelParser.ts @@ -4,9 +4,8 @@ import * as t from '@babel/types'; import { Identifier, ImportDeclaration, V8IntrinsicIdentifier } from '@babel/types'; import { glob } from 'glob'; import path from 'path'; -import { JAVASCRIPT_FILE_REGEX, TYPESCRIPT_FILE_REGEX } from '../../../constants/language'; -import { ImportedModule } from '../importedModule'; -import { SupportedLanguage } from '../ossVulnerabilityCountService'; +import { JAVASCRIPT_FILE_REGEX, TYPESCRIPT_FILE_REGEX } from '../../../../common/constants/languageConsts'; +import { ImportedModule, Language } from '../../../../common/types'; import { ModuleParser } from './moduleParser'; const PARSE_PLUGINS: ParserPlugin[] = [ @@ -23,7 +22,7 @@ const PARSE_JS_PLUGINS: ParserPlugin[] = ['flow', ...PARSE_PLUGINS]; const PARSE_TS_PLUGINS: ParserPlugin[] = ['typescript', ...PARSE_PLUGINS]; export class BabelParser extends ModuleParser { - getModules(fileName: string, source: string, language: SupportedLanguage): ImportedModule[] { + getModules(fileName: string, source: string, language: Language): ImportedModule[] { const modules: ImportedModule[] = []; const visitor = this.getVisitor(fileName, modules); @@ -70,8 +69,8 @@ export class BabelParser extends ModuleParser { }; } - private parse(source: string, language: SupportedLanguage) { - const plugins = language === SupportedLanguage.TypeScript ? PARSE_TS_PLUGINS : PARSE_JS_PLUGINS; + private parse(source: string, language: Language) { + const plugins = language === Language.TypeScript ? PARSE_TS_PLUGINS : PARSE_JS_PLUGINS; return jsParse(source, { sourceType: 'module', plugins, diff --git a/src/snyk/snykOss/services/vulnerabilityCount/parsers/htmlParser.ts b/src/snyk/snykOss/services/vulnerabilityCount/parsers/htmlParser.ts index 0f6620acf..dc6254306 100644 --- a/src/snyk/snykOss/services/vulnerabilityCount/parsers/htmlParser.ts +++ b/src/snyk/snykOss/services/vulnerabilityCount/parsers/htmlParser.ts @@ -1,5 +1,5 @@ import * as htmlparser2 from 'htmlparser2'; -import { ImportedModule } from '../importedModule'; +import { ImportedModule } from '../../../../common/types'; import { ModuleParser } from './moduleParser'; class SupportedSources { diff --git a/src/snyk/snykOss/services/vulnerabilityCount/parsers/moduleParser.ts b/src/snyk/snykOss/services/vulnerabilityCount/parsers/moduleParser.ts index 913db1e8e..55fcb46f9 100644 --- a/src/snyk/snykOss/services/vulnerabilityCount/parsers/moduleParser.ts +++ b/src/snyk/snykOss/services/vulnerabilityCount/parsers/moduleParser.ts @@ -1,6 +1,5 @@ -import { ImportedModule } from '../importedModule'; -import { SupportedLanguage } from '../ossVulnerabilityCountService'; +import { ImportedModule, Language } from '../../../../common/types'; export abstract class ModuleParser { - abstract getModules(fileName: string, source: string, language: SupportedLanguage): ImportedModule[]; + abstract getModules(fileName: string, source: string, language: Language): ImportedModule[]; } diff --git a/src/snyk/snykOss/services/vulnerabilityCount/parsers/moduleParserProvider.ts b/src/snyk/snykOss/services/vulnerabilityCount/parsers/moduleParserProvider.ts deleted file mode 100644 index 14e87f9fc..000000000 --- a/src/snyk/snykOss/services/vulnerabilityCount/parsers/moduleParserProvider.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { SupportedLanguage } from '../ossVulnerabilityCountService'; -import { BabelParser } from './babelParser'; -import { HtmlParser } from './htmlParser'; -import { ModuleParser } from './moduleParser'; -import { PackageJsonParser } from './packageJsonParser'; - -export class ModuleParserProvider { - static getInstance(language: SupportedLanguage): ModuleParser | undefined { - if ([SupportedLanguage.TypeScript, SupportedLanguage.JavaScript].includes(language)) { - return new BabelParser(); - } else if (language === SupportedLanguage.PJSON) { - return new PackageJsonParser(); - } else if (language === SupportedLanguage.HTML) { - return new HtmlParser(); - } - - return undefined; - } -} diff --git a/src/snyk/snykOss/services/vulnerabilityCount/parsers/packageJsonParser.ts b/src/snyk/snykOss/services/vulnerabilityCount/parsers/packageJsonParser.ts index 042d8fe53..d6097e2ec 100644 --- a/src/snyk/snykOss/services/vulnerabilityCount/parsers/packageJsonParser.ts +++ b/src/snyk/snykOss/services/vulnerabilityCount/parsers/packageJsonParser.ts @@ -1,4 +1,5 @@ -import { ImportedModule, Range } from '../importedModule'; +import { ILog } from '../../../../common/logger/interfaces'; +import { ImportedModule, OssRange } from '../../../../common/types'; import { ModuleParser } from './moduleParser'; export type PackageJsonDependencies = { @@ -15,6 +16,10 @@ export interface DependencyLines { } export class PackageJsonParser extends ModuleParser { + constructor(private readonly logger: ILog) { + super(); + } + getModules(fileName: string, source: string): ImportedModule[] { const packages = []; @@ -23,23 +28,34 @@ export class PackageJsonParser extends ModuleParser { lines.push(line); }); - const pjson = JSON.parse(source) as PackageJson; - const depLines = this.findDependecyLines(pjson.dependencies, lines); - if (!depLines) { - return []; + let pjson = null; + + try { + pjson = JSON.parse(source) as PackageJson; + } catch (err: unknown) { + if (err instanceof Error) { + this.logger.error(`Failed to parse package.json file: ${err.message}`); + } } - for (const dependency in pjson.dependencies) { - const loc = this.findRange(dependency, depLines.lines, depLines.offset); + if (pjson) { + const depLines = this.findDependecyLines(pjson.dependencies, lines); + if (!depLines) { + return []; + } - const module = { - fileName, - name: dependency, - loc, - line: loc?.start.line, - } as ImportedModule; + for (const dependency in pjson.dependencies) { + const loc = this.findRange(dependency, depLines.lines, depLines.offset); - packages.push(module); + const module = { + fileName, + name: dependency, + loc, + line: loc?.start.line, + } as ImportedModule; + + packages.push(module); + } } return packages; @@ -52,7 +68,20 @@ export class PackageJsonParser extends ModuleParser { } const depLineIndex = lines.indexOf(depStartLine) + 1; - const depLines = lines.slice(depLineIndex, depLineIndex + Object.keys(dependencies).length); + let dependenciesLength = Object.keys(dependencies).length; + const emptyLineTabRegex = /(\t)$/; + for (let i = depLineIndex + 1; i < lines.length - 1; i++) { + if (emptyLineTabRegex.test(lines[i]) || !lines[i] || !lines[i].trim()) { + dependenciesLength += 1; + continue; + } + + // end of dependencies. + if (lines[i].includes('}')) { + break; + } + } + const depLines = lines.slice(depLineIndex, depLineIndex + dependenciesLength); return { lines: depLines, @@ -60,7 +89,7 @@ export class PackageJsonParser extends ModuleParser { }; } - private findRange(dependency: string, lines: string[], offset: number): Range | undefined { + private findRange(dependency: string, lines: string[], offset: number): OssRange | undefined { const line = lines.find(x => x.includes('"' + dependency + '"')); if (!line) { return; diff --git a/src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.ts b/src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.ts index 678bb17fc..b7df8f849 100644 --- a/src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.ts +++ b/src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.ts @@ -1,8 +1,8 @@ +import { Language } from '../../../common/types'; import { isResultCliError, OssFileResult, OssResultBody, OssVulnerability } from '../../ossResult'; import { OssService } from '../ossService'; import { ImportedModule, ModuleVulnerabilityCount, SeverityCounts, TestedImportedModule } from './importedModule'; import { NpmModuleInfoFetchService } from './npmModuleInfoFetchService'; -import { SupportedLanguage } from './ossVulnerabilityCountService'; export class ModuleVulnerabilityCountProvider { constructor( @@ -10,7 +10,7 @@ export class ModuleVulnerabilityCountProvider { private readonly npmModuleInfoFetchService: NpmModuleInfoFetchService, ) {} - async getVulnerabilityCount(module: ImportedModule, language: SupportedLanguage): Promise { + async getVulnerabilityCount(module: ImportedModule, language: Language): Promise { const notCalculated = { name: module.name, fileName: module.fileName, @@ -19,14 +19,14 @@ export class ModuleVulnerabilityCountProvider { hasCount: false, }; - if ([SupportedLanguage.TypeScript, SupportedLanguage.JavaScript, SupportedLanguage.PJSON].includes(language)) { + if ([Language.TypeScript, Language.JavaScript, Language.PJSON].includes(language)) { const ossResult = this.ossService.getResultArray(); if (!ossResult) { return notCalculated; } return this.mapOssResult(module, ossResult); - } else if (language == SupportedLanguage.HTML) { + } else if (language == Language.HTML) { const testedModuleInfo = await this.npmModuleInfoFetchService.getModuleVulnerabilityInfo(module); return this.mapTestedImportedModule(testedModuleInfo); } diff --git a/src/test/unit/advisor/services/advisorProvider.test.ts b/src/test/unit/advisor/services/advisorProvider.test.ts new file mode 100644 index 000000000..d2299138f --- /dev/null +++ b/src/test/unit/advisor/services/advisorProvider.test.ts @@ -0,0 +1,40 @@ +import { notStrictEqual, strictEqual } from 'assert'; +import AdvisorProvider from '../../../../snyk/advisor/services/advisorProvider'; +import { ImportedModule } from '../../../../snyk/common/types'; +import { LoggerMock } from '../../mocks/logger.mock'; +import { advisorApiClientStub, postFake } from './advisorStubs'; + +suite('Advisor score provider.', () => { + let advisorProvider: AdvisorProvider; + + const sampleFilePath = 'C:\\git\\project\\test.js'; + const sampleModuleName = 'mongo-express'; + const sampleImportedModule = { + fileName: sampleFilePath, + name: sampleModuleName, + string: 'const x = require("mongo-express")', + line: 1, + loc: { + start: { + line: 1, + column: 16, + }, + end: { + line: 1, + column: 29, + }, + }, + } as ImportedModule; + + const loggerMock = new LoggerMock(); + setup(() => { + advisorProvider = new AdvisorProvider(advisorApiClientStub, loggerMock); + }); + + test('AdvisorProvider returns scores', async () => { + const scores = await advisorProvider.getScores([sampleImportedModule]); + + notStrictEqual(scores, []); + strictEqual(postFake.returned({ data: [] }), true); + }); +}); diff --git a/src/test/unit/advisor/services/advisorService.test.ts b/src/test/unit/advisor/services/advisorService.test.ts new file mode 100644 index 000000000..dd473107a --- /dev/null +++ b/src/test/unit/advisor/services/advisorService.test.ts @@ -0,0 +1,105 @@ +import { strictEqual } from 'assert'; +import sinon from 'sinon'; +import AdvisorProvider from '../../../../snyk/advisor/services/advisorProvider'; +import { AdvisorService } from '../../../../snyk/advisor/services/advisorService'; +import { IConfiguration } from '../../../../snyk/common/configuration/configuration'; +import { PJSON } from '../../../../snyk/common/constants/languageConsts'; +import { HoverAdapter } from '../../../../snyk/common/vscode/hover'; +import { IMarkdownStringAdapter } from '../../../../snyk/common/vscode/markdownString'; +import { ThemeColorAdapter } from '../../../../snyk/common/vscode/theme'; +import { TextDocument, TextDocumentChangeEvent, TextEditor } from '../../../../snyk/common/vscode/types'; +import { LoggerMock } from '../../mocks/logger.mock'; +import { advisorApiClientStub, advisorStubLanguages, advisorStubWindow, advisorStubWorkspace } from './advisorStubs'; + +suite('Advisor AdvisorService', () => { + let advisorService: AdvisorService; + let advisorProvider: AdvisorProvider; + + const loggerMock = new LoggerMock(); + + setup(() => { + advisorProvider = new AdvisorProvider(advisorApiClientStub, loggerMock); + advisorProvider.getScores = sinon.fake.returns([]); + advisorService = new AdvisorService( + advisorStubWindow, + advisorStubLanguages, + advisorProvider, + loggerMock, + advisorStubWorkspace, + advisorApiClientStub, + {} as ThemeColorAdapter, + {} as HoverAdapter, + {} as IMarkdownStringAdapter, + { + getPreviewFeatures: () => { + return { + advisor: true, + }; + }, + } as unknown as IConfiguration, + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Attaches onDidChangeTextDocument listener on activation', async () => { + advisorStubWindow.onDidChangeActiveTextEditor = sinon.fake(); + const onDidChangeTextDocumentSpy = sinon.fake(); + advisorStubWorkspace.onDidChangeTextDocument = onDidChangeTextDocumentSpy; + + await advisorService.activate(); + + strictEqual(onDidChangeTextDocumentSpy.calledOnce, true); + }); + + test('Attaches onDidChangeActiveTextEditor listener on activation', async () => { + advisorStubWorkspace.onDidChangeTextDocument = sinon.fake(); + + const onDidChangeActiveTextEditor = sinon.fake(); + advisorStubWindow.onDidChangeActiveTextEditor = onDidChangeActiveTextEditor; + + await advisorService.activate(); + + strictEqual(onDidChangeActiveTextEditor.calledOnce, true); + }); + + test('Processes file if active editor is opened on activation', async () => { + advisorStubWindow.getActiveTextEditor = () => + ({ + document: { + fileName: 'package.json', + languageId: PJSON, + getText: () => ``, + }, + } as unknown as TextEditor); + + advisorStubWorkspace.onDidChangeTextDocument = sinon.fake(); + advisorStubWindow.onDidChangeActiveTextEditor = sinon.fake(); + + const getActiveTextEditorSpy = sinon.spy(advisorStubWindow, 'getActiveTextEditor'); + const processFileSpy = sinon.spy(advisorService, 'processScores'); + + await advisorService.activate(); + + strictEqual(getActiveTextEditorSpy.called, true); + strictEqual(processFileSpy.calledOnce, true); + }); + + test("Doesn't process if file is language not supported", async () => { + const document = { + fileName: 'C:\\git\\project\\test.java', + languageId: 'java', + } as TextDocument; + const ev: TextDocumentChangeEvent = { + document, + contentChanges: [], + }; + + const processFileSpy = sinon.spy(advisorService, 'processScores'); + await advisorService.handleEditorEvent(ev.document); + + strictEqual(processFileSpy.called, false); + }); +}); diff --git a/src/test/unit/advisor/services/advisorStubs.ts b/src/test/unit/advisor/services/advisorStubs.ts new file mode 100644 index 000000000..3720f5d96 --- /dev/null +++ b/src/test/unit/advisor/services/advisorStubs.ts @@ -0,0 +1,40 @@ +import sinon from 'sinon'; +import { AdvisorRegistry } from '../../../../snyk/advisor/advisorTypes'; +import { IAdvisorApiClient } from '../../../../snyk/advisor/services/advisorApiClient'; +import { PJSON } from '../../../../snyk/common/constants/languageConsts'; +import { IVSCodeLanguages } from '../../../../snyk/common/vscode/languages'; +import { TextEditor } from '../../../../snyk/common/vscode/types'; +import { IVSCodeWindow } from '../../../../snyk/common/vscode/window'; +import { IVSCodeWorkspace } from '../../../../snyk/common/vscode/workspace'; + +const postFake = sinon.stub().returns({ + data: [], +}); +const scoresStub = sinon.stub().resolves({ data: [] }); +const advisorApiClientStub: IAdvisorApiClient = { + post: postFake, + apiPath: '', + getAdvisorUrl: function (registry: AdvisorRegistry): string { + return `${registry}/scores`; + }, +}; + +const advisorStubWorkspace: IVSCodeWorkspace = {} as IVSCodeWorkspace; +const advisorStubWindow: IVSCodeWindow = { + createTextEditorDecorationType: sinon.fake(), + getActiveTextEditor: sinon.fake.returns({ + document: { + fileName: 'C:\\git\\project\\package.json', + languageId: PJSON, + getText: () => '', + }, + } as TextEditor), + getVisibleTextEditors: sinon.fake.returns([]), +} as unknown as IVSCodeWindow; +const advisorStubLanguages: IVSCodeLanguages = { + createDiagnosticCollection: sinon.fake(), + registerCodeActionsProvider: sinon.fake(), + registerHoverProvider: sinon.fake(), +} as unknown as IVSCodeLanguages; + +export { scoresStub, postFake, advisorApiClientStub, advisorStubWorkspace, advisorStubLanguages, advisorStubWindow }; diff --git a/src/test/unit/common/configuration.test.ts b/src/test/unit/common/configuration.test.ts index 312a44540..3f86f16da 100644 --- a/src/test/unit/common/configuration.test.ts +++ b/src/test/unit/common/configuration.test.ts @@ -148,12 +148,14 @@ suite('Configuration', () => { deepStrictEqual(configuration.getPreviewFeatures(), { reportFalsePositives: false, + advisor: false, } as PreviewFeatures); }); test('Preview features: some features enabled', () => { const previewFeatures = { reportFalsePositives: true, + advisor: false, } as PreviewFeatures; const workspace = stubWorkspaceConfiguration(FEATURES_PREVIEW_SETTING, previewFeatures); diff --git a/src/test/unit/snykOss/services/vulnerabilityCount/parsers/babelParser.test.ts b/src/test/unit/snykOss/services/vulnerabilityCount/parsers/babelParser.test.ts index 09f291ea1..4846065a1 100644 --- a/src/test/unit/snykOss/services/vulnerabilityCount/parsers/babelParser.test.ts +++ b/src/test/unit/snykOss/services/vulnerabilityCount/parsers/babelParser.test.ts @@ -1,5 +1,5 @@ import { strictEqual } from 'assert'; -import { SupportedLanguage } from '../../../../../../snyk/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService'; +import { Language } from '../../../../../../snyk/common/types'; import { BabelParser } from '../../../../../../snyk/snykOss/services/vulnerabilityCount/parsers/babelParser'; /** @@ -17,7 +17,7 @@ suite('Babel Parser', () => { var _ = require('lodash'); var express = require('express'); `; - const modules = parser.getModules('test.js', source, SupportedLanguage.JavaScript); + const modules = parser.getModules('test.js', source, Language.JavaScript); strictEqual(modules.length, 2); strictEqual(modules[0].name, 'lodash'); @@ -33,7 +33,7 @@ suite('Babel Parser', () => { import * as express from 'express'; `; - const modules = parser.getModules('test.ts', source, SupportedLanguage.TypeScript); + const modules = parser.getModules('test.ts', source, Language.TypeScript); strictEqual(modules.length, 2); strictEqual(modules[0].name, 'lodash'); @@ -48,7 +48,7 @@ suite('Babel Parser', () => { import { _ } from 'lodash'; `; - const modules = parser.getModules('test.ts', source, SupportedLanguage.TypeScript); + const modules = parser.getModules('test.ts', source, Language.TypeScript); strictEqual(modules.length, 1); strictEqual(modules[0].name, 'lodash'); @@ -60,7 +60,7 @@ suite('Babel Parser', () => { import { test1, test2 } from 'lodash'; `; - const modules = parser.getModules('test.ts', source, SupportedLanguage.TypeScript); + const modules = parser.getModules('test.ts', source, Language.TypeScript); strictEqual(modules.length, 1); strictEqual(modules[0].name, 'lodash'); @@ -72,7 +72,7 @@ suite('Babel Parser', () => { import { test1 as x, test2 as y } from 'lodash'; `; - const modules = parser.getModules('test.ts', source, SupportedLanguage.TypeScript); + const modules = parser.getModules('test.ts', source, Language.TypeScript); strictEqual(modules.length, 1); strictEqual(modules[0].name, 'lodash'); @@ -85,7 +85,7 @@ suite('Babel Parser', () => { import { test3 as z, test4 as e } from 'express'; `; - const modules = parser.getModules('test.ts', source, SupportedLanguage.TypeScript); + const modules = parser.getModules('test.ts', source, Language.TypeScript); strictEqual(modules.length, 2); strictEqual(modules[0].name, 'lodash'); @@ -104,7 +104,7 @@ suite('Babel Parser', () => { })(); `; - const modules = parser.getModules('test.ts', source, SupportedLanguage.TypeScript); + const modules = parser.getModules('test.ts', source, Language.TypeScript); strictEqual(modules.length, 1); strictEqual(modules[0].name, 'express'); @@ -116,7 +116,7 @@ suite('Babel Parser', () => { import * as _ from 'lodash'; import * as express from 'express'; `; - const modules = parser.getModules('test.ts', source, SupportedLanguage.TypeScript); + const modules = parser.getModules('test.ts', source, Language.TypeScript); strictEqual(modules.length, 2); strictEqual(modules[0].name, 'lodash'); diff --git a/src/test/unit/snykOss/services/vulnerabilityCount/parsers/moduleParserProvider.test.ts b/src/test/unit/snykOss/services/vulnerabilityCount/parsers/moduleParserProvider.test.ts index 4e2c5291d..6bd505aba 100644 --- a/src/test/unit/snykOss/services/vulnerabilityCount/parsers/moduleParserProvider.test.ts +++ b/src/test/unit/snykOss/services/vulnerabilityCount/parsers/moduleParserProvider.test.ts @@ -1,16 +1,17 @@ import { strictEqual } from 'assert'; -import { SupportedLanguage } from '../../../../../../snyk/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService'; +import { ModuleParserProvider } from '../../../../../../snyk/common/services/moduleParserProvider'; +import { Language } from '../../../../../../snyk/common/types'; import { BabelParser } from '../../../../../../snyk/snykOss/services/vulnerabilityCount/parsers/babelParser'; -import { ModuleParserProvider } from '../../../../../../snyk/snykOss/services/vulnerabilityCount/parsers/moduleParserProvider'; +import { LoggerMock } from '../../../../mocks/logger.mock'; suite('OSS ModuleParserProvider', () => { test('Babel parser is returned for TS and JS', () => { - const parser = ModuleParserProvider.getInstance(SupportedLanguage.JavaScript); + const parser = ModuleParserProvider.getInstance(Language.JavaScript, new LoggerMock()); strictEqual(parser instanceof BabelParser, true); }); test('Undefined instance is returned for non-supported language', () => { - const parser = ModuleParserProvider.getInstance(-1); + const parser = ModuleParserProvider.getInstance(-1, new LoggerMock()); strictEqual(parser, undefined); }); }); diff --git a/src/test/unit/snykOss/services/vulnerabilityCount/parsers/packageJsonParser.test.ts b/src/test/unit/snykOss/services/vulnerabilityCount/parsers/packageJsonParser.test.ts index 72c88492f..4faf537ad 100644 --- a/src/test/unit/snykOss/services/vulnerabilityCount/parsers/packageJsonParser.test.ts +++ b/src/test/unit/snykOss/services/vulnerabilityCount/parsers/packageJsonParser.test.ts @@ -1,5 +1,6 @@ import { strictEqual } from 'assert'; import { PackageJsonParser } from '../../../../../../snyk/snykOss/services/vulnerabilityCount/parsers/packageJsonParser'; +import { LoggerMock } from '../../../../mocks/logger.mock'; /** * ES Import statements documentation: @@ -8,7 +9,7 @@ import { PackageJsonParser } from '../../../../../../snyk/snykOss/services/vulne suite("'package.json' Parser", () => { let parser: PackageJsonParser; setup(() => { - parser = new PackageJsonParser(); + parser = new PackageJsonParser(new LoggerMock()); }); test('Basic', () => { diff --git a/src/test/unit/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.test.ts b/src/test/unit/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.test.ts index 9b1598d07..8f79644e1 100644 --- a/src/test/unit/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.test.ts +++ b/src/test/unit/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.test.ts @@ -1,11 +1,11 @@ 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 { NpmModuleInfoFetchService } from '../../../../../snyk/snykOss/services/vulnerabilityCount/npmModuleInfoFetchService'; -import { SupportedLanguage } from '../../../../../snyk/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService'; import { ModuleVulnerabilityCountProvider } from '../../../../../snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider'; suite('OSS ModuleVulnerabilityCountProvider', () => { @@ -64,16 +64,13 @@ suite('OSS ModuleVulnerabilityCountProvider', () => { test('Not calculated if JS/TS results are not provided', async () => { ossService.getResultArray = () => undefined; - const tsCount = await vulnerabilityCountProvider.getVulnerabilityCount( - sampleImportedModule, - SupportedLanguage.TypeScript, - ); + const tsCount = await vulnerabilityCountProvider.getVulnerabilityCount(sampleImportedModule, Language.TypeScript); const jsCount = await vulnerabilityCountProvider.getVulnerabilityCount( { ...sampleImportedModule, fileName: 'test.ts', }, - SupportedLanguage.JavaScript, + Language.JavaScript, ); strictEqual(jsCount.hasCount, false); @@ -84,10 +81,7 @@ suite('OSS ModuleVulnerabilityCountProvider', () => { ossService.getResultArray = () => sampleOssResults; ossService.getUniqueVulnerabilities = () => sampleOssResults[0].vulnerabilities; - const count = await vulnerabilityCountProvider.getVulnerabilityCount( - sampleImportedModule, - SupportedLanguage.TypeScript, - ); + const count = await vulnerabilityCountProvider.getVulnerabilityCount(sampleImportedModule, Language.TypeScript); strictEqual(count.hasCount, true); strictEqual(count.count, 2); @@ -110,7 +104,7 @@ suite('OSS ModuleVulnerabilityCountProvider', () => { ossService.getResultArray = () => sampleOssResults; ossService.getUniqueVulnerabilities = () => sampleOssResults[0].vulnerabilities; - const count = await vulnerabilityCountProvider.getVulnerabilityCount(sampleImportedModule, SupportedLanguage.PJSON); + const count = await vulnerabilityCountProvider.getVulnerabilityCount(sampleImportedModule, Language.PJSON); strictEqual(count.hasCount, true); }); @@ -135,10 +129,7 @@ suite('OSS ModuleVulnerabilityCountProvider', () => { ossService.getResultArray = () => ossResultsWithIndirectVulnerability; ossService.getUniqueVulnerabilities = () => ossResultsWithIndirectVulnerability[0].vulnerabilities; - const count = await vulnerabilityCountProvider.getVulnerabilityCount( - sampleImportedModule, - SupportedLanguage.TypeScript, - ); + const count = await vulnerabilityCountProvider.getVulnerabilityCount(sampleImportedModule, Language.TypeScript); strictEqual(count.hasCount, true); strictEqual(count.count, 1); @@ -166,10 +157,7 @@ suite('OSS ModuleVulnerabilityCountProvider', () => { ossService.getResultArray = () => ossResultsWithMultipleVersionsVulnerability; ossService.getUniqueVulnerabilities = () => ossResultsWithMultipleVersionsVulnerability[0].vulnerabilities; - const count = await vulnerabilityCountProvider.getVulnerabilityCount( - sampleImportedModule, - SupportedLanguage.TypeScript, - ); + const count = await vulnerabilityCountProvider.getVulnerabilityCount(sampleImportedModule, Language.TypeScript); strictEqual(count.version, version); }); @@ -195,10 +183,7 @@ suite('OSS ModuleVulnerabilityCountProvider', () => { ossService.getResultArray = () => ossResultsWithMultipleVersionsVulnerability; ossService.getUniqueVulnerabilities = () => ossResultsWithMultipleVersionsVulnerability[0].vulnerabilities; - const count = await vulnerabilityCountProvider.getVulnerabilityCount( - sampleImportedModule, - SupportedLanguage.TypeScript, - ); + const count = await vulnerabilityCountProvider.getVulnerabilityCount(sampleImportedModule, Language.TypeScript); strictEqual(count.version, undefined); });