diff --git a/.eslintrc.js b/.eslintrc.js index 9cb163203..090414c59 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -52,5 +52,6 @@ module.exports = { "no-unsafe-finally": "error", "new-parens": "error", "no-throw-literal": "error", + "no-useless-catch": "off" } } \ No newline at end of file diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml index 01586c02d..ab9dceb52 100644 --- a/.github/workflows/stage.yml +++ b/.github/workflows/stage.yml @@ -104,7 +104,7 @@ jobs: const response = await github.request('POST /repos/' + repo_name + '/releases', { tag_name: '${{ steps.bump.outputs.version }}', name: '${{ steps.bump.outputs.version }}', - prerelease: true, + prerelease: false, generate_release_notes: true }) core.setOutput('upload_url', response.data.upload_url) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3aef5582c..924d1969d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -34,5 +34,5 @@ "out": true // set this to false to include "out" folder in search results }, "typescript.tsdk": "./node_modules/typescript/lib", - "redhat.telemetry.enabled": true // we want to use the TS server from our node_modules folder to control its version + "redhat.telemetry.enabled": true } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f0930c0cb..26615de29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,22 @@ # Change Log +## 0.8.0 (Dec 11th 2023) +- informational - Service Preview release of Red Hat Dependency Analytics (RHDA) extension. +- informational - Configuration names for all supported executable paths in the extension settings have changed. These executable paths are only used for the analysis. +- enhancement - Added support for error observation by using Sentry. +- enhancement - Support for more complex SPDX SBOM relationships. +- enhancement - Added recommendations and remediations in the _Quick Fix..._ tab. +- fixes - Fixed an issue where unique Snyk vulnerability information was not being displayed in the Dependency Analytics report. See [PR#217](https://github.com/RHEcosystemAppEng/exhort/pull/217) for details. +- fixes - Better valid and invalid token alert messages for the Snyk vulnerability information provider. See [PR#218](https://github.com/RHEcosystemAppEng/exhort/pull/218) for details. +- fixes - Fixed analysis report discrepancies between Red Hat Dependency Analytics and Snyk’s analytics. See [PR#219](https://github.com/RHEcosystemAppEng/exhort/pull/219) for details. +- fixes - Fixed the Go and Python package links so they point to their specific package manager website. ## 0.7.3 (Nov 8th 2023) -- enhancement - Support for Golang and Python ecosystems. See [#656](https://github.com/fabric8-analytics/fabric8-analytics-vscode-extension/pull/656) +- enhancement - Support for Golang and Python ecosystems. See [PR#656](https://github.com/fabric8-analytics/fabric8-analytics-vscode-extension/pull/656) for details. - enhancement - A new setting for Python and Go environments to restrict package analysis when there is a package version mis-match between the environment and the manifest file. See the [Features section](https://github.com/fabric8-analytics/fabric8-analytics-vscode-extension/blob/master/README.md#features) of the README for more information. ## 0.7.0 (Sep 11th 2023) -- fixes - Improved overall performance and stability with the analysis report. - informational - Alpha release of the new Red Hat Dependency Analytics (RHDA) extension. -- informational - Code base refactoring from CRDA to RHDA alpha. See [#636](https://github.com/fabric8-analytics/fabric8-analytics-vscode-extension/pull/636) +- informational - Code base refactoring from CRDA to RHDA alpha. See [PR#636](https://github.com/fabric8-analytics/fabric8-analytics-vscode-extension/pull/636) for details. - informational - Currently no support for Python and Go, but coming soon. +- fixes - Improved overall performance and stability with the analysis report. ## 0.3.10 (May 22th 2022) - fixes - Extension breaks for Go version 1.17. See [#608](https://github.com/fabric8-analytics/fabric8-analytics-vscode-extension/pull/608) - fixes - Retry failed stack analysis requests. See [#609](https://github.com/fabric8-analytics/fabric8-analytics-vscode-extension/pull/609) diff --git a/README.md b/README.md index b981f25ef..59100e8f3 100644 --- a/README.md +++ b/README.md @@ -101,13 +101,23 @@ The default path is `/tmp/redhatDependencyAnalyticsReport.html`. - **Component analysis**
Upon opening a manifest file, such as a `pom.xml`, `package.json`, `go.mod` or `requirements.txt` file, a scan starts the analysis process. The scan provides immediate inline feedback on detected security vulnerabilities for your application's dependencies. - Such dependencies are appropriately underlined in red, and hovering over it gives you a short summary of the security concern. + Such dependencies are appropriately underlined in red, and hovering over it gives you a short summary of the security concern from Snyk. The summary has the full package name, version number, the amount of known security vulnerabilities, and the highest severity status of said vulnerabilities. **NOTE:** Add the `target` folder to your `.gitignore` file to exclude it from Git monitoring. ![ Animated screenshot showing the inline reporting feature of Red Hat Dependency Analytics ](images/screencasts/component-analysis.gif) +- **Recommendations and remediations** +
After running a detailed analysis report on a specific component version, you can view recommendations and remediations by using the _Quick Fix..._ menu. + If there is a Red Hat recommended package version available, you can replace your version with Red Hat's version. + + ![ Animated screenshot showing how to access the _Quick Fix..._ menu, and switching to a Red Hat recommended package version ](images/screencasts/quickfix.gif) + +
**IMPORTANT:** For Maven projects only, when analyzing a `pom.xml` file. + You must configure Red Hat's generally available (GA) repository to use the recommendations or remediations. + Add this repository, `https://maven.repository.redhat.com/ga/`, to your project's configuration. + - **Excluding dependencies with `exhortignore`**
You can exclude a package from analysis by marking the package for exclusion. If you wish to ignore vulnerabilities for a dependency in a `pom.xml` file, you must add `exhortignore` as a comment against the dependency, group id, artifact id, or version scopes of that particular dependency in the manifest file. diff --git a/images/screencasts/component-analysis.gif b/images/screencasts/component-analysis.gif index e7b27c5fa..d8c60ef74 100644 Binary files a/images/screencasts/component-analysis.gif and b/images/screencasts/component-analysis.gif differ diff --git a/images/screencasts/quickfix.gif b/images/screencasts/quickfix.gif index 981294744..7c588268e 100644 Binary files a/images/screencasts/quickfix.gif and b/images/screencasts/quickfix.gif differ diff --git a/images/screenshots/extension-workspace-settings.png b/images/screenshots/extension-workspace-settings.png index f78f80099..0ce3916ad 100644 Binary files a/images/screenshots/extension-workspace-settings.png and b/images/screenshots/extension-workspace-settings.png differ diff --git a/package-lock.json b/package-lock.json index f3adaf6ec..6c44eeb79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "fabric8-analytics", - "version": "0.7.5", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fabric8-analytics", - "version": "0.7.5", + "version": "0.8.0", "license": "Apache-2.0", "dependencies": { - "@fabric8-analytics/fabric8-analytics-lsp-server": "^0.7.1-ea.18", + "@fabric8-analytics/fabric8-analytics-lsp-server": "^0.9.0", "@redhat-developer/vscode-redhat-telemetry": "^0.7.0", - "@RHEcosystemAppEng/exhort-javascript-api": "^0.0.2-ea.49", + "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.1-ea.5", "fs": "^0.0.1-security", "path": "^0.12.7", "vscode-languageclient": "^8.1.0" @@ -44,13 +44,48 @@ "vscode": "^1.76.0" } }, + "../exhort-javascript-api": { + "name": "@RHEcosystemAppEng/exhort-javascript-api", + "version": "0.0.2-ea.50", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "^7.23.2", + "@cyclonedx/cyclonedx-library": "^4.0.0", + "fast-xml-parser": "^4.2.4", + "packageurl-js": "^1.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "exhort-javascript-api": "dist/src/cli.js" + }, + "devDependencies": { + "@babel/core": "^7.23.2", + "@openapitools/openapi-generator-cli": "^2.6.0", + "@types/node": "^20.3.1", + "babel-plugin-rewire": "^1.2.0", + "c8": "^8.0.0", + "chai": "^4.3.7", + "eslint": "^8.42.0", + "eslint-plugin-editorconfig": "^4.0.3", + "mocha": "^10.2.0", + "msw": "^1.3.2", + "sinon": "^15.1.2", + "sinon-chai": "^3.7.0", + "typescript": "^5.1.3" + }, + "engines": { + "node": ">= 18.0.0", + "npm": ">= 9.0.0" + } + }, "../fabric8-analytics-lsp-server": { "name": "@fabric8-analytics/fabric8-analytics-lsp-server", - "version": "0.7.1-ea.13", + "version": "0.8.1-ea.6", "extraneous": true, "license": "Apache-2.0", "dependencies": { - "@RHEcosystemAppEng/exhort-javascript-api": "^0.0.2-ea.43", + "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.1-ea.4", "@xml-tools/ast": "^5.0.5", "@xml-tools/parser": "^1.0.11", "json-to-ast": "^2.1.0", @@ -64,7 +99,6 @@ "@types/chai": "^4.3.7", "@types/mocha": "^10.0.2", "@types/node": "^20.8.4", - "@types/node-fetch": "^2.6.6", "@types/uuid": "^9.0.5", "@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/parser": "^6.7.5", @@ -73,7 +107,9 @@ "fake-exec": "^1.1.0", "mocha": "^10.2.0", "nyc": "^15.1.0", + "sinon": "^17.0.1", "ts-node": "^10.9.1", + "typedoc": "^0.25.3", "typescript": "^5.2.2" } }, @@ -531,12 +567,12 @@ } }, "node_modules/@fabric8-analytics/fabric8-analytics-lsp-server": { - "version": "0.7.1-ea.18", - "resolved": "https://npm.pkg.github.com/download/@fabric8-analytics/fabric8-analytics-lsp-server/0.7.1-ea.18/356799181c1ee1e0db382b6ae738fcd1c5c67e5e", - "integrity": "sha512-dJg3TpcNDkIiMNEZZusZ8cBhnMlhM4r4TmV2O52HL/7IFBKYpXheKqv6qio+2S5QNyrKJx6f/fei9qgpQDct7A==", + "version": "0.9.0", + "resolved": "https://npm.pkg.github.com/download/@fabric8-analytics/fabric8-analytics-lsp-server/0.9.0/cd862688446a5e05593ea2ac41dd18751e4f0f6c", + "integrity": "sha512-V8aTvDrB5bl/LIo3856r6SmJsF9IkBzyslij0DvQHVHaASyK4drpwtRNzueIMZ+eRxS64ZqAt2VmM7xXJes6SA==", "license": "Apache-2.0", "dependencies": { - "@RHEcosystemAppEng/exhort-javascript-api": "^0.0.2-ea.49", + "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.1-ea.5", "@xml-tools/ast": "^5.0.5", "@xml-tools/parser": "^1.0.11", "json-to-ast": "^2.1.0", @@ -913,9 +949,9 @@ } }, "node_modules/@RHEcosystemAppEng/exhort-javascript-api": { - "version": "0.0.2-ea.49", - "resolved": "https://npm.pkg.github.com/download/@RHEcosystemAppEng/exhort-javascript-api/0.0.2-ea.49/0380891b685a3eb30653010a6849669553ea4bb9", - "integrity": "sha512-APOe3QjMjE+Dx9ASZPN97Tpxq/fTvHic9IBTvfCeWhIK5M/WJ562B6U/YG7qjQmHfUur8jHXZOQpJ/bXfNBKDA==", + "version": "0.1.1-ea.5", + "resolved": "https://npm.pkg.github.com/download/@RHEcosystemAppEng/exhort-javascript-api/0.1.1-ea.5/2cbd94284336733cbb2e3aa8c931ae6b2c1f5d8d", + "integrity": "sha512-VBIbeUvBw8DjTNHrBTVvqt2UrdHybApTsopXurlLWjsbmK4R8SJO0zUiQXcxDOyxY40moiNr2gncrcMN3tTRVA==", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.23.2", @@ -5071,9 +5107,9 @@ } }, "node_modules/packageurl-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-1.2.0.tgz", - "integrity": "sha512-JFoZnz1maKB0hTjn0YrmqRLgiU825SkbA370oe9ERcsKsj1EcBpe+CDo1EK9mrHc+18Hi5NmZbmXFQtP7YZEbw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-1.2.1.tgz", + "integrity": "sha512-cZ6/MzuXaoFd16/k0WnwtI298UCaDHe/XlSh85SeOKbGZ1hq0xvNbx3ILyCMyk7uFQxl6scF3Aucj6/EO9NwcA==" }, "node_modules/pako": { "version": "1.0.11", diff --git a/package.json b/package.json index f18c4e7d5..90326ebef 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "fabric8-analytics", "displayName": "Red Hat Dependency Analytics", "description": "Provides insights on security vulnerabilities in your application dependencies.", - "version": "0.7.5", + "version": "0.8.0", "author": "Red Hat", "publisher": "redhat", "preview": true, @@ -187,7 +187,7 @@ "redHatDependencyAnalytics.exhortSnykToken": { "type": "string", "default": "", - "description": "Red Hat Dependency Analytics server authentication token for Snyk.", + "description": "Red Hat Dependency Analytics authentication token for Snyk.", "scope": "window" }, "redHatDependencyAnalytics.matchManifestVersions": { @@ -202,43 +202,43 @@ "description": "Path to a local file where the Red Hat Dependency Analytics report will be saved.", "scope": "window" }, - "mvn.executable.path": { + "redHatDependencyAnalytics.mvn.executable.path": { "type": "string", "default": "", "description": "Specifies absolute path of mvn executable.", "scope": "window" }, - "npm.executable.path": { + "redHatDependencyAnalytics.npm.executable.path": { "type": "string", "default": "", "description": "Specifies absolute path of npm executable.", "scope": "window" }, - "go.executable.path": { + "redHatDependencyAnalytics.go.executable.path": { "type": "string", "default": "", "description": "Specifies absolute path of go executable.", "scope": "window" }, - "python3.executable.path": { + "redHatDependencyAnalytics.python3.executable.path": { "type": "string", "default": "", "description": "Specifies absolute path of python3 executable, python3 takes precedence over python.", "scope": "window" }, - "pip3.executable.path": { + "redHatDependencyAnalytics.pip3.executable.path": { "type": "string", "default": "", "description": "Specifies absolute path of pip3 executable, pip3 takes precedence over pip.", "scope": "window" }, - "python.executable.path": { + "redHatDependencyAnalytics.python.executable.path": { "type": "string", "default": "", "description": "Specifies absolute path of python executable, python3 takes precedence over python.", "scope": "window" }, - "pip.executable.path": { + "redHatDependencyAnalytics.pip.executable.path": { "type": "string", "default": "", "description": "Specifies absolute path of pip executable, pip3 takes precedence over pip.", @@ -284,9 +284,9 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "@fabric8-analytics/fabric8-analytics-lsp-server": "^0.7.1-ea.18", + "@fabric8-analytics/fabric8-analytics-lsp-server": "^0.9.0", "@redhat-developer/vscode-redhat-telemetry": "^0.7.0", - "@RHEcosystemAppEng/exhort-javascript-api": "^0.0.2-ea.49", + "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.1-ea.5", "fs": "^0.0.1-security", "path": "^0.12.7", "vscode-languageclient": "^8.1.0" diff --git a/src/DepOutputChannel.ts b/src/DepOutputChannel.ts deleted file mode 100644 index c91fa0602..000000000 --- a/src/DepOutputChannel.ts +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -import * as vscode from 'vscode'; -import { Titles } from './constants'; - -export class DepOutputChannel { - outputChannel: vscode.OutputChannel; - constructor(channelName: string = Titles.EXT_TITLE) { - this.outputChannel = vscode.window.createOutputChannel(channelName); - } - - getOutputChannel(): vscode.OutputChannel { - this.outputChannel.clear(); - return this.outputChannel; - } - - showOutputChannel(): void { - this.outputChannel.show(); - } - - clearOutputChannel(): void { - this.outputChannel.clear(); - } - - addMsgOutputChannel(msg: string): void { - this.outputChannel.append(msg); - } -} diff --git a/src/caNotification.ts b/src/caNotification.ts index 1a9eacee5..792c5136e 100644 --- a/src/caNotification.ts +++ b/src/caNotification.ts @@ -1,58 +1,159 @@ 'use strict'; +/** + * Interface representing the data structure for a Component Analysis (CA) Notification. + */ interface CANotificationData { - data: string; + errorMessage: string; done: boolean; uri: string; diagCount: number; - vulnCount: number; + vulnCount: Map; } +/** + * Provides functionality for generating a Component Analysis (CA) Notification. + */ class CANotification { - private data: string; - private done: boolean; - private uri: string; - private diagCount: number; - private vulnCount: number; + private readonly errorMessage: string; + private readonly done: boolean; + private readonly uri: string; + private readonly diagCount: number; + private readonly vulnCount: Map; + private readonly totalVulnCount: number; + private static readonly VULNERABILITY = 'vulnerability'; + private static readonly VULNERABILITIES = 'vulnerabilities'; + + private static readonly SYNC_SPIN = 'sync~spin'; + private static readonly WARNING = 'warning'; + private static readonly SHIELD = 'shield'; + private static readonly CHECK = 'check'; + + /** + * Creates an instance of CANotification based on the given data. + * @param respData The data used to create the notification. + */ constructor(respData: CANotificationData) { - this.data = respData.data; + this.errorMessage = respData.errorMessage || ''; this.done = respData.done === true; this.uri = respData.uri; this.diagCount = respData.diagCount || 0; - this.vulnCount = respData.vulnCount || 0; + this.vulnCount = respData.vulnCount || new Map(); + this.totalVulnCount = Object.values(this.vulnCount).reduce((sum, cv) => sum + cv, 0); + } + + /** + * Determines if a singular or plural form of a word should be used based on the number of vulnerabilities. + * @param num The number of vulnerabilities to evaluate. + * @returns The appropriate string representing either 'vulnerability' or 'vulnerabilities'. + * @private + */ + private singularOrPlural(num: number): string { + return num === 1 ? CANotification.VULNERABILITY : CANotification.VULNERABILITIES; + } + + /** + * Capitalizes each word in a given string. + * @param inputString The string to be capitalized. + * @returns The string with each word capitalized. + * @private + */ + private capitalizeEachWord(inputString: string): string { + return inputString.replace(/\b\w/g, (match) => match.toUpperCase()); + } + + /** + * Generates text for the total vulnerability count. + * @returns Text representing the total number of vulnerabilities. + * @private + */ + private vulnCountText(): string { + return this.totalVulnCount > 0 ? `${this.totalVulnCount} direct ${this.singularOrPlural(this.totalVulnCount)}` : `no ${CANotification.VULNERABILITIES}`; + } + + /** + * Generates the text for the in-progress status. + * @returns Text representing the in-progress status. + * @private + */ + private inProgressText(): string { + return `$(${CANotification.SYNC_SPIN}) RHDA analysis in progress`; + } + + /** + * Generates the text for the warning status for amount of vulnerabilities found. + * @returns Text representing the amount of vulnerabilities found. + * @private + */ + private warningText(): string { + return `$(${CANotification.WARNING}) ${this.vulnCountText()} found for all the providers combined`; + } + + /** + * Generates the default text for the status. + * @returns Default text for the status. + * @private + */ + private defaultText(): string { + return `$(${CANotification.SHIELD})$(${CANotification.CHECK})`; } + /** + * Retrieves the error message associated with the notification. + * @returns The error message, if available; otherwise, an empty string. + */ + public errorMsg(): string { + return this.errorMessage; + } + + /** + * Retrieves the URI associated with the notification. + * @returns The URI string. + */ public origin(): string { return this.uri; } + /** + * Checks if the analysis is done. + * @returns A boolean value indicating if the analysis is done or not. + */ public isDone(): boolean { return this.done; } + /** + * Checks if any diagnostic have been found in the analysis. + * @returns A boolean indicating if diagnostics have been found. + */ public hasWarning(): boolean { return this.diagCount > 0; } + /** + * Generates the text to display in the notification popup. + * @returns The text content for the popup notification. + */ public popupText(): string { - // replace texts inside $(..) - return this.statusText().replace(/\$\((.*?)\)/g, ''); - } - - private vulnCountText(): string { - const vulns = this.vulnCount; - return vulns > 0 ? `${vulns} ${vulns === 1 ? 'vulnerability' : 'vulnerabilities'}` : `no vulnerabilities`; + const text: string = Object.entries(this.vulnCount) + .map(([provider, vulnCount]) => `Found ${vulnCount} direct ${this.singularOrPlural(vulnCount)} for ${this.capitalizeEachWord(provider)} Provider.`) + .join(' '); + return text || this.warningText().replace(/\$\((.*?)\)/g, ''); } + /** + * Generates the text to display in the status bar. + * @returns The text content for the status bar. + */ public statusText(): string { if (!this.isDone()) { - return `$(sync~spin) Dependency analysis in progress`; + return this.inProgressText(); } if (this.hasWarning()) { - return `$(warning) Found ${this.vulnCountText()}`; + return this.warningText(); } - return `$(shield)$(check)`; + return this.defaultText(); } } diff --git a/src/caStatusBarProvider.ts b/src/caStatusBarProvider.ts index 44c78ced4..a47eec172 100644 --- a/src/caStatusBarProvider.ts +++ b/src/caStatusBarProvider.ts @@ -5,13 +5,24 @@ import { Disposable } from 'vscode-languageclient'; import { PromptText } from './constants'; import * as commands from './commands'; +/** + * Provides status bar functionality for the extension. + */ class CAStatusBarProvider implements Disposable { private statusBarItem: StatusBarItem; + /** + * Creates an instance of the CAStatusBarProvider class. + */ constructor() { this.statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 0); } + /** + * Displays summary information in the status bar. + * @param text The text to display in the status bar. + * @param uri The URI associated with the summary. + */ public showSummary(text: string, uri: string): void { this.statusBarItem.text = text; this.statusBarItem.command = { @@ -23,17 +34,26 @@ class CAStatusBarProvider implements Disposable { this.statusBarItem.show(); } + /** + * Sets an error message in the status bar indicating a failed RHDA analysis. + */ public setError(): void { - this.statusBarItem.text = `$(error) Dependency analysis has failed`; + this.statusBarItem.text = `$(error) RHDA analysis has failed`; this.statusBarItem.command = { title: PromptText.LSP_FAILURE_TEXT, command: commands.TRIGGER_STACK_LOGS, }; } + /** + * Disposes of the status bar item. + */ public dispose(): void { this.statusBarItem.dispose(); } } +/** + * Provides an instance of CAStatusBarProvider for use across the extension. + */ export const caStatusBarProvider: CAStatusBarProvider = new CAStatusBarProvider(); diff --git a/src/commands.ts b/src/commands.ts index 7f5062c9b..d3600da01 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -4,13 +4,9 @@ * Commonly used commands to trigger Stack Analysis and supporting actions */ export const TRIGGER_FULL_STACK_ANALYSIS = 'fabric8.stackAnalysis'; -export const TRIGGER_FULL_STACK_ANALYSIS_FROM_STATUS_BAR = - 'fabric8.stackAnalysisFromStatusBar'; -export const TRIGGER_FULL_STACK_ANALYSIS_FROM_EXPLORER = - 'fabric8.stackAnalysisFromExplorer'; -export const TRIGGER_FULL_STACK_ANALYSIS_FROM_PIE_BTN = - 'fabric8.stackAnalysisFromPieBtn'; -export const TRIGGER_FULL_STACK_ANALYSIS_FROM_EDITOR = - 'fabric8.stackAnalysisFromEditor'; +export const TRIGGER_FULL_STACK_ANALYSIS_FROM_STATUS_BAR = 'fabric8.stackAnalysisFromStatusBar'; +export const TRIGGER_FULL_STACK_ANALYSIS_FROM_EXPLORER = 'fabric8.stackAnalysisFromExplorer'; +export const TRIGGER_FULL_STACK_ANALYSIS_FROM_PIE_BTN = 'fabric8.stackAnalysisFromPieBtn'; +export const TRIGGER_FULL_STACK_ANALYSIS_FROM_EDITOR = 'fabric8.stackAnalysisFromEditor'; export const TRIGGER_STACK_LOGS = 'fabric8.fabric8AnalyticsStackLogs'; -export const TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION = 'fabric8.RHRepositoryRecommendationNotification'; +export const TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION = 'fabric8.RHRepositoryRecommendationNotification'; \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index ed067b87d..44cb7ee40 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,55 +2,110 @@ import * as vscode from 'vscode'; -export function getApiConfig(): any { - return vscode.workspace.getConfiguration('redHatDependencyAnalytics'); -} +import { GlobalState, defaultRhdaReportFilePath } from './constants'; +import * as commands from './commands'; +import { getTelemetryId } from './redhatTelemetry'; -export function getMvnExecutable(): string { - const mvnPath: string = vscode.workspace - .getConfiguration('mvn.executable') - .get('path'); - return mvnPath ? mvnPath : 'mvn'; -} +/** + * Represents the configuration settings for the extension. + */ +class Config { + telemetryId: string; + triggerFullStackAnalysis: string; + triggerRHRepositoryRecommendationNotification: string; + utmSource: string; + exhortSnykToken: string; + matchManifestVersions: string; + exhortMvnPath: string; + exhortNpmPath: string; + exhortGoPath: string; + exhortPython3Path: string; + exhortPip3Path: string; + exhortPythonPath: string; + exhortPipPath: string; + rhdaReportFilePath: string; -export function getNpmExecutable(): string { - const npmPath: string = vscode.workspace - .getConfiguration('npm.executable') - .get('path'); - return npmPath ? npmPath : 'npm'; -} + private readonly DEFAULT_MVN_EXECUTABLE = 'mvn'; + private readonly DEFAULT_NPM_EXECUTABLE = 'npm'; + private readonly DEFAULT_GO_EXECUTABLE = 'go'; + private readonly DEFAULT_PYTHON3_EXECUTABLE = 'python3'; + private readonly DEFAULT_PIP3_EXECUTABLE = 'pip3'; + private readonly DEFAULT_PYTHON_EXECUTABLE = 'python'; + private readonly DEFAULT_PIP_EXECUTABLE = 'pip'; -export function getGoExecutable(): string { - const goPath: string = vscode.workspace - .getConfiguration('go.executable') - .get('path'); - return goPath ? goPath : 'go'; -} + /** + * Creates an instance of the Config class. + * Initializes the instance with default extension settings. + */ + constructor() { + this.loadData(); + this.setProcessEnv(); + } -export function getPython3Executable(): string { - const python3Path: string = vscode.workspace - .getConfiguration('python3.executable') - .get('path'); - return python3Path ? python3Path : 'python3'; -} + /** + * Retrieves RHDA configuration settings. + * @returns The RHDA configuration settings. + * @private + */ + private getRhdaConfig(): any { + return vscode.workspace.getConfiguration('redHatDependencyAnalytics'); + } -export function getPip3Executable(): string { - const pip3Path: string = vscode.workspace - .getConfiguration('pip3.executable') - .get('path'); - return pip3Path ? pip3Path : 'pip3'; -} + /** + * Loads configuration settings. + */ + loadData() { + const rhdaConfig = this.getRhdaConfig(); -export function getPythonExecutable(): string { - const pythonPath: string = vscode.workspace - .getConfiguration('python.executable') - .get('path'); - return pythonPath ? pythonPath : 'python'; -} + this.triggerFullStackAnalysis = commands.TRIGGER_FULL_STACK_ANALYSIS; + this.triggerRHRepositoryRecommendationNotification = commands.TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION; + this.utmSource = GlobalState.UTM_SOURCE; + this.exhortSnykToken = rhdaConfig.exhortSnykToken; + this.matchManifestVersions = rhdaConfig.matchManifestVersions ? 'true' : 'false'; + this.rhdaReportFilePath = rhdaConfig.redHatDependencyAnalyticsReportFilePath || defaultRhdaReportFilePath; + this.exhortMvnPath = rhdaConfig.mvn.executable.path || this.DEFAULT_MVN_EXECUTABLE; + this.exhortNpmPath = rhdaConfig.npm.executable.path || this.DEFAULT_NPM_EXECUTABLE; + this.exhortGoPath = rhdaConfig.go.executable.path || this.DEFAULT_GO_EXECUTABLE; + this.exhortPython3Path = rhdaConfig.python3.executable.path || this.DEFAULT_PYTHON3_EXECUTABLE; + this.exhortPip3Path = rhdaConfig.pip3.executable.path || this.DEFAULT_PIP3_EXECUTABLE; + this.exhortPythonPath = rhdaConfig.python.executable.path || this.DEFAULT_PYTHON_EXECUTABLE; + this.exhortPipPath = rhdaConfig.pip.executable.path || this.DEFAULT_PIP_EXECUTABLE; + } + + /** + * Sets process environment variables based on configuration settings. + * @private + */ + private setProcessEnv() { + process.env['VSCEXT_TRIGGER_FULL_STACK_ANALYSIS'] = this.triggerFullStackAnalysis; + process.env['VSCEXT_TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION'] = this.triggerRHRepositoryRecommendationNotification; + process.env['VSCEXT_UTM_SOURCE'] = this.utmSource; + process.env['VSCEXT_EXHORT_SNYK_TOKEN'] = this.exhortSnykToken; + process.env['VSCEXT_MATCH_MANIFEST_VERSIONS'] = this.matchManifestVersions; + process.env['VSCEXT_EXHORT_MVN_PATH'] = this.exhortMvnPath; + process.env['VSCEXT_EXHORT_NPM_PATH'] = this.exhortNpmPath; + process.env['VSCEXT_EXHORT_GO_PATH'] = this.exhortGoPath; + process.env['VSCEXT_EXHORT_PYTHON3_PATH'] = this.exhortPython3Path; + process.env['VSCEXT_EXHORT_PIP3_PATH'] = this.exhortPip3Path; + process.env['VSCEXT_EXHORT_PYTHON_PATH'] = this.exhortPythonPath; + process.env['VSCEXT_EXHORT_PIP_PATH'] = this.exhortPipPath; + process.env['EXHORT_DEV_MODE'] = GlobalState.EXHORT_DEV_MODE; + } -export function getPipExecutable(): string { - const pipPath: string = vscode.workspace - .getConfiguration('pip.executable') - .get('path'); - return pipPath ? pipPath : 'pip'; + /** + * Authorizes the RHDA (Red Hat Dependency Analytics) service. + * @param context The extension context for authorization. + */ + async authorizeRHDA(context) { + this.telemetryId = await getTelemetryId(context); + process.env['VSCEXT_TELEMETRY_ID'] = this.telemetryId; + } } + +/** + * The global configuration object for the extension. + */ +const globalConfig = new Config(); + +export { globalConfig }; + diff --git a/src/constants.ts b/src/constants.ts index 3c3decaa8..521e687a5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,13 +8,13 @@ export enum GlobalState { // to store the UTM source for tracking purposes UTM_SOURCE = 'vscode', // to store the current exhort environment mode - EXHORT_DEV_MODE = 'false' + EXHORT_DEV_MODE = 'true' } export enum StatusMessages { WIN_ANALYZING_DEPENDENCIES = 'Analyzing application dependencies...', WIN_GENERATING_DEPENDENCIES = 'Generating Red Hat Dependency Analytics report...', - WIN_SUCCESS_DEPENDENCY_ANALYSIS = 'Successfully generated Red Hat Dependency Analytics report...', + WIN_SUCCESS_DEPENDENCY_ANALYSIS = 'Successfully generated Red Hat Dependency Analytics report', WIN_FAILURE_DEPENDENCY_ANALYSIS = 'Unable to generate Red Hat Dependency Analytics report', WIN_SHOW_LOGS = 'No output channel has been created for Red Hat Dependency Analytics', NO_SUPPORTED_MANIFEST = 'No supported manifest file found to be analyzed.', @@ -39,7 +39,7 @@ export const registrationURL = 'https://app.snyk.io/signup/?utm_medium=Partner&u // URL to Snyk webpage export const snykURL = 'https://app.snyk.io/login?utm_campaign=Code-Ready-Analytics-2020&utm_source=code_ready&code_ready=FF1B53D9-57BE-4613-96D7-1D06066C38C9'; // default Redhat Dependency Analytics report file path -export const defaultRedhatDependencyAnalyticsReportFilePath = '/tmp/redhatDependencyAnalyticsReport.html'; +export const defaultRhdaReportFilePath = '/tmp/redhatDependencyAnalyticsReport.html'; // Red Hat GA Repository export const redhatMavenRepository = 'https://maven.repository.redhat.com/ga/'; // Red Hat GA Repository documentation diff --git a/src/contextHandler.ts b/src/contextHandler.ts deleted file mode 100644 index 60f54342d..000000000 --- a/src/contextHandler.ts +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -import { GlobalState } from './constants'; -import * as config from './config'; -import { getRedHatService } from '@redhat-developer/vscode-redhat-telemetry/lib'; - -export const loadEnvironmentData = () => { - - const apiConfig = config.getApiConfig(); - - process.env['VSCEXT_PROVIDE_FULLSTACK_ACTION'] = 'true'; - process.env['VSCEXT_UTM_SOURCE'] = GlobalState.UTM_SOURCE; - process.env['VSCEXT_EXHORT_DEV_MODE'] = GlobalState.EXHORT_DEV_MODE; - process.env['VSCEXT_EXHORT_SNYK_TOKEN'] = apiConfig.exhortSnykToken; - process.env['VSCEXT_MATCH_MANIFEST_VERSIONS'] = apiConfig.matchManifestVersions ? 'true' : 'false'; - process.env['VSCEXT_EXHORT_MVN_PATH'] = config.getMvnExecutable(); - process.env['VSCEXT_EXHORT_NPM_PATH'] = config.getNpmExecutable(); - process.env['VSCEXT_EXHORT_GO_PATH'] = config.getGoExecutable(); - process.env['VSCEXT_EXHORT_PYTHON3_PATH'] = config.getPython3Executable(); - process.env['VSCEXT_EXHORT_PIP3_PATH'] = config.getPip3Executable(); - process.env['VSCEXT_EXHORT_PYTHON_PATH'] = config.getPythonExecutable(); - process.env['VSCEXT_EXHORT_PIP_PATH'] = config.getPipExecutable(); -}; - -async function setTelemetryid(context) { - const redhatService = await getRedHatService(context); - const redhatIdProvider = await redhatService.getIdProvider(); - const redhatUuid = await redhatIdProvider.getRedHatUUID(); - process.env['VSCEXT_TELEMETRY_ID'] = redhatUuid; -} - -export const loadContextData = async context => { - try { - await setTelemetryid(context); - - loadEnvironmentData(); - - return true; - } catch (error) { - console.log(error); - return false; - } -}; diff --git a/src/depOutputChannel.ts b/src/depOutputChannel.ts new file mode 100644 index 000000000..09e8ed63e --- /dev/null +++ b/src/depOutputChannel.ts @@ -0,0 +1,51 @@ +'use strict'; + +import * as vscode from 'vscode'; +import { Titles } from './constants'; + +/** + * Class representing an output channel for displaying messages in VS Code. + */ +export class DepOutputChannel { + outputChannel: vscode.OutputChannel; + + /** + * Creates an instance of DepOutputChannel. + * @param channelName The name of the output channel. Defaults to the extension title. + */ + constructor(channelName: string = Titles.EXT_TITLE) { + this.outputChannel = vscode.window.createOutputChannel(channelName); + } + + /** + * Retrieves the VS Code OutputChannel instance. + * Clears the channel before returning. + * @returns The VS Code OutputChannel instance. + */ + getOutputChannel(): vscode.OutputChannel { + this.outputChannel.clear(); + return this.outputChannel; + } + + /** + * Shows the output channel in the VS Code UI. + */ + showOutputChannel(): void { + this.outputChannel.show(); + } + + /** + * Clears the content of the output channel. + */ + clearOutputChannel(): void { + this.outputChannel.clear(); + } + + /** + * Appends a message to the output channel. + * @param msg The message to append to the output channel. + */ + addMsgOutputChannel(msg: string): void { + this.outputChannel.append(msg); + } +} diff --git a/src/dependencyReportPanel.ts b/src/dependencyReportPanel.ts index abab9c6ed..81e47606f 100644 --- a/src/dependencyReportPanel.ts +++ b/src/dependencyReportPanel.ts @@ -1,20 +1,19 @@ import * as vscode from 'vscode'; import * as templates from './template'; -import { Titles, defaultRedhatDependencyAnalyticsReportFilePath } from './constants'; -import * as config from './config'; +import { Titles } from './constants'; +import { globalConfig } from './config'; import * as fs from 'fs'; const loaderTmpl = templates.LOADER_TEMPLATE; const errorTmpl = templates.ERROR_TEMPLATE; /** - * Manages cat coding webview panels + * Manages the webview panel for RHDA reports. + * Tracks the currently panel. Only allow a single panel to exist at a time. */ export class DependencyReportPanel { - /** - * Track the currently panel. Only allow a single panel to exist at a time. - */ + public static currentPanel: DependencyReportPanel | undefined; public static readonly viewType = 'stackReport'; @@ -23,6 +22,9 @@ export class DependencyReportPanel { private readonly _panel: vscode.WebviewPanel; private _disposables: vscode.Disposable[] = []; + /** + * Creates or shows the webview panel. + */ public static createOrShowWebviewPanel() { const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn @@ -85,6 +87,10 @@ export class DependencyReportPanel { ); } + /** + * Updates the panel with the provided data. + * @param data Data to update the panel with. + */ public doUpdatePanel(data: any) { if (data && /<\s*html[^>]*>/i.test(data)) { DependencyReportPanel.data = data; @@ -95,6 +101,9 @@ export class DependencyReportPanel { } } + /** + * Disposes the panel. + */ public dispose() { DependencyReportPanel.currentPanel = undefined; @@ -110,18 +119,35 @@ export class DependencyReportPanel { } } + /** + * Checks if the panel is visible. + * @returns A boolean indicating if the panel is visible. + */ public getPanelVisibility(): boolean { return this._panel.visible; } + /** + * Retrieves the HTML content of the webview panel. + * @returns The HTML content of the webview panel. + */ public getWebviewPanelHtml(): string { return this._panel.webview.html; } + /** + * Reveals the webview panel. + * @param column The column to reveal the panel in. + * @private + */ private _revealWebviewPanel(column: vscode.ViewColumn) { this._panel.reveal(column); } + /** + * Updates the webview panel content. + * @private + */ private _updateWebViewPanel() { const output = DependencyReportPanel.data; if (output && /<\s*html[^>]*>/i.test(output)) { @@ -131,12 +157,16 @@ export class DependencyReportPanel { } } + /** + * Disposes the RHDA report file from local directory. + * @private + */ private _disposeReport() { - const apiConfig = config.getApiConfig(); - if (fs.existsSync(apiConfig.redHatDependencyAnalyticsReportFilePath || defaultRedhatDependencyAnalyticsReportFilePath)) { + const reportFilePath = globalConfig.rhdaReportFilePath; + if (fs.existsSync(reportFilePath)) { // Delete temp stackAnalysisReport file - fs.unlinkSync(apiConfig.redHatDependencyAnalyticsReportFilePath || defaultRedhatDependencyAnalyticsReportFilePath); - console.log(`File ${apiConfig.redHatDependencyAnalyticsReportFilePath || defaultRedhatDependencyAnalyticsReportFilePath} has been deleted.`); + fs.unlinkSync(reportFilePath); + console.log(`File ${reportFilePath} has been deleted.`); } } } \ No newline at end of file diff --git a/src/stackAnalysisService.ts b/src/exhortServices.ts similarity index 53% rename from src/stackAnalysisService.ts rename to src/exhortServices.ts index b96db7078..f7d5fc325 100644 --- a/src/stackAnalysisService.ts +++ b/src/exhortServices.ts @@ -3,7 +3,13 @@ import * as vscode from 'vscode'; import exhort from '@RHEcosystemAppEng/exhort-javascript-api'; -export const exhortApiStackAnalysis = (pathToManifest, options) => { +/** + * Performs RHDA stack analysis based on the provided manifest path and options. + * @param pathToManifest The path to the manifest file for analysis. + * @param options Additional options for the analysis. + * @returns A promise resolving to the stack analysis report in HTML format. + */ +function stackAnalysisService(pathToManifest, options) { return new Promise(async (resolve, reject) => { try { // Get stack analysis in HTML format @@ -13,9 +19,15 @@ export const exhortApiStackAnalysis = (pathToManifest, options) => { reject(error); } }); -}; +} -export const getSnykTokenValidationService = async (options) => { +/** + * Performes RHDA token validation based on the provided options and displays messages based on the validation status. + * @param options The options for token validation. + * @param source The source for which the token is being validated. Example values: 'Snyk', 'OSS Index'. + * @returns A promise resolving after validating the token. + */ +async function tokenValidationService(options, source) { try { // Get token validation status code @@ -24,19 +36,19 @@ export const getSnykTokenValidationService = async (options) => { if ( tokenValidationStatus === 200 ) { - vscode.window.showInformationMessage('Snyk Token Validated Successfully'); + vscode.window.showInformationMessage(`${source} Token Validated Successfully`); } else if ( tokenValidationStatus === 400 ) { - vscode.window.showWarningMessage(`Missing token. Please provide a valid Snyk Token in the extension workspace settings. Status: ${tokenValidationStatus}`); + vscode.window.showWarningMessage(`Missing token. Please provide a valid ${source} Token in the extension workspace settings. Status: ${tokenValidationStatus}`); } else if ( tokenValidationStatus === 401 ) { - vscode.window.showWarningMessage(`Invalid token. Please provide a valid Snyk Token in the extension workspace settings. Status: ${tokenValidationStatus}`); + vscode.window.showWarningMessage(`Invalid token. Please provide a valid ${source} Token in the extension workspace settings. Status: ${tokenValidationStatus}`); } else if ( tokenValidationStatus === 403 ) { - vscode.window.showWarningMessage(`Forbidden. The token does not have permissions. Please provide a valid Snyk Token in the extension workspace settings. Status: ${tokenValidationStatus}`); + vscode.window.showWarningMessage(`Forbidden. The token does not have permissions. Please provide a valid ${source} Token in the extension workspace settings. Status: ${tokenValidationStatus}`); } else if ( tokenValidationStatus === 429 ) { @@ -47,4 +59,6 @@ export const getSnykTokenValidationService = async (options) => { } catch (error) { vscode.window.showErrorMessage(`Failed to validate token, Error: ${error}`); } -}; \ No newline at end of file +} + +export { stackAnalysisService, tokenValidationService }; \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 60e21948b..afffe8851 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,35 +7,55 @@ import { ServerOptions, TransportKind } from 'vscode-languageclient/node'; - import * as path from 'path'; import * as commands from './commands'; -import { GlobalState, extensionQualifiedId, registrationURL, redhatMavenRepository, redhatMavenRepositoryDocumentationURL } from './constants'; -import * as multimanifestmodule from './multimanifestmodule'; -import { loadContextData } from './contextHandler'; +import { GlobalState, extensionQualifiedId, redhatMavenRepository, redhatMavenRepositoryDocumentationURL } from './constants'; +import { generateRHDAReport } from './stackAnalysis'; +import { globalConfig } from './config'; import { StatusMessages, PromptText } from './constants'; import { caStatusBarProvider } from './caStatusBarProvider'; import { CANotification } from './caNotification'; -import { DepOutputChannel } from './DepOutputChannel'; +import { DepOutputChannel } from './depOutputChannel'; import { record, startUp, TelemetryActions } from './redhatTelemetry'; +import { validateSnykToken } from './tokenValidation'; let lspClient: LanguageClient; export let outputChannelDep: DepOutputChannel; +/** + * Activates the extension upon launch. + * @param context - The extension context. + */ export function activate(context: vscode.ExtensionContext) { startUp(context); - const disposableFullStack = vscode.commands.registerCommand( + + // show welcome message after first install or upgrade + showUpdateNotification(context); + + const disposableStackAnalysisCommand = vscode.commands.registerCommand( commands.TRIGGER_FULL_STACK_ANALYSIS, - (uri: vscode.Uri) => { + async (uri: vscode.Uri) => { + // uri will be null in case the user has used the context menu/file explorer + const fileUri = uri ? uri : vscode.window.activeTextEditor.document.uri; try { - // uri will be null in case the user has used the context menu/file explorer - const fileUri = uri ? uri : vscode.window.activeTextEditor.document.uri; - multimanifestmodule.redhatDependencyAnalyticsReportFlow(context, fileUri); + await generateRHDAReport(context, fileUri); + record(context, TelemetryActions.vulnerabilityReportDone, { manifest: path.basename(fileUri.fsPath), fileName: path.basename(fileUri.fsPath) }); } catch (error) { - // Throw a custom error message when the command execution fails - throw new Error(`Running the contributed command: '${commands.TRIGGER_FULL_STACK_ANALYSIS}' failed.`); + vscode.window.showErrorMessage(error.message); + record(context, TelemetryActions.vulnerabilityReportFailed, { manifest: path.basename(fileUri.fsPath), fileName: path.basename(fileUri.fsPath), error: error.message }); + } + } + ); + + const disposableStackLogsCommand = vscode.commands.registerCommand( + commands.TRIGGER_STACK_LOGS, + () => { + if (outputChannelDep) { + outputChannelDep.showOutputChannel(); + } else { + vscode.window.showInformationMessage(StatusMessages.WIN_SHOW_LOGS); } } ); @@ -51,26 +71,12 @@ export function activate(context: vscode.ExtensionContext) { } ); - const disposableStackLogs = vscode.commands.registerCommand( - commands.TRIGGER_STACK_LOGS, - () => { - if (outputChannelDep) { - outputChannelDep.showOutputChannel(); - } else { - vscode.window.showInformationMessage(StatusMessages.WIN_SHOW_LOGS); - } - } - ); - registerStackAnalysisCommands(context); - // show welcome message after first install or upgrade - showUpdateNotification(context); - - loadContextData(context).then(status => { - if (status) { + globalConfig.authorizeRHDA(context) + .then(() => { // Create output channel - outputChannelDep = initOutputChannel(); + outputChannelDep = new DepOutputChannel(); // The server is implemented in node const serverModule = context.asAbsolutePath( path.join('dist', 'server.js') @@ -92,7 +98,6 @@ export function activate(context: vscode.ExtensionContext) { // Options to control the language client const clientOptions: LanguageClientOptions = { - // Register the server for xml, json documents documentSelector: [ { scheme: 'file', language: 'json' }, { scheme: 'file', language: 'xml' }, @@ -106,11 +111,7 @@ export function activate(context: vscode.ExtensionContext) { configurationSection: 'redHatDependencyAnalyticsServer', // Notify the server about file changes to '.clientrc files contained in the workspace fileEvents: vscode.workspace.createFileSystemWatcher('**/.clientrc'), - }, - initializationOptions: { - triggerFullStackAnalysis: commands.TRIGGER_FULL_STACK_ANALYSIS, - triggerRHRepositoryRecommendationNotification: commands.TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION - }, + } }; // Create the language client and start the client. @@ -121,14 +122,9 @@ export function activate(context: vscode.ExtensionContext) { clientOptions ); lspClient.start().then(() => { - const notifiedFiles = new Set(); - const canShowPopup = (notification: CANotification): boolean => { - const hasAlreadyShown = notifiedFiles.has(notification.origin()); - return notification.hasWarning() && !hasAlreadyShown; - }; const showVulnerabilityFoundPrompt = async (msg: string, fileName: string) => { - const selection = await vscode.window.showWarningMessage(`${msg}. Powered by [Snyk](${registrationURL})`, PromptText.FULL_STACK_PROMPT_TEXT); + const selection = await vscode.window.showWarningMessage(`${msg}`, PromptText.FULL_STACK_PROMPT_TEXT); if (selection === PromptText.FULL_STACK_PROMPT_TEXT) { vscode.commands.executeCommand(commands.TRIGGER_FULL_STACK_ANALYSIS); record(context, TelemetryActions.vulnerabilityReportPopupOpened, { manifest: fileName, fileName: fileName }); @@ -141,47 +137,49 @@ export function activate(context: vscode.ExtensionContext) { lspClient.onNotification('caNotification', respData => { const notification = new CANotification(respData); caStatusBarProvider.showSummary(notification.statusText(), notification.origin()); - if (canShowPopup(notification)) { + if (notification.hasWarning()) { showVulnerabilityFoundPrompt(notification.popupText(), path.basename(notification.origin())); record(context, TelemetryActions.componentAnalysisDone, { manifest: path.basename(notification.origin()), fileName: path.basename(notification.origin()) }); - // prevent further popups. - notifiedFiles.add(notification.origin()); } }); - lspClient.onNotification('caError', respData => { - const notification = new CANotification(respData); + lspClient.onNotification('caError', errorData => { + const notification = new CANotification(errorData); caStatusBarProvider.setError(); - vscode.window.showErrorMessage(respData.data); - record(context, TelemetryActions.componentAnalysisFailed, { manifest: path.basename(notification.origin()), fileName: path.basename(notification.origin()), error: respData.data }); - }); - lspClient.onNotification('caSimpleWarning', msg => { - vscode.window.showWarningMessage(msg); + // Since CA is an automated feature, only warning message will be shown on failure + vscode.window.showWarningMessage(notification.errorMsg()); + + // Record telemetry event + record(context, TelemetryActions.componentAnalysisFailed, { manifest: path.basename(notification.origin()), fileName: path.basename(notification.origin()), error: notification.errorMsg() }); }); }); context.subscriptions.push( + disposableStackAnalysisCommand, + disposableStackLogsCommand, rhRepositoryRecommendationNotification, - disposableFullStack, - disposableStackLogs, caStatusBarProvider, ); - } - }); + }) + .catch(error => { + vscode.window.showErrorMessage(`Failed to Authorize Red Hat Dependency Analytics extension: ${error.message}`); + throw (error); + }); vscode.workspace.onDidChangeConfiguration((event) => { + + globalConfig.loadData(); + if (event.affectsConfiguration('redHatDependencyAnalytics.exhortSnykToken')) { - multimanifestmodule.triggerTokenValidation('snyk'); + validateSnykToken(); } - // add more token providers here... }); } -export function initOutputChannel(): DepOutputChannel { - const outputChannelDepInit = new DepOutputChannel(); - return outputChannelDepInit; -} - +/** + * Deactivates the extension. + * @returns A `Thenable` for void. + */ export function deactivate(): Thenable { if (!lspClient) { return undefined; @@ -189,45 +187,58 @@ export function deactivate(): Thenable { return lspClient.stop(); } +/** + * Shows an update notification if the extension has been updated to a new version. + * @param context - The extension context. + * @returns A Promise that resolves once the notification has been displayed if needed. + */ async function showUpdateNotification(context: vscode.ExtensionContext) { - // Retrive current and previous version string to show welcome message + const packageJSON = vscode.extensions.getExtension(extensionQualifiedId).packageJSON; const version = packageJSON.version; const previousVersion = context.globalState.get(GlobalState.VERSION); - // Nothing to display + if (version === previousVersion) { return; } - // store current version into localStorage context.globalState.update(GlobalState.VERSION, version); - const actions: vscode.MessageItem[] = [{ title: 'README' }, { title: 'Release Notes' }]; - - const displayName = packageJSON.displayName; const result = await vscode.window.showInformationMessage( - `${displayName} has been updated to v${version} — check out what's new!`, - ...actions + `${packageJSON.displayName} has been updated to v${version} — check out what's new!`, + 'README', + 'Release Notes' ); - if (result !== null) { - if (result === actions[0]) { + if (result !== undefined) { + if (result === 'README') { await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(packageJSON.homepage)); - } else if (result === actions[1]) { - await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(`${packageJSON.repository.url}/releases/tag/${version}`)); + } else if (result === 'Release Notes') { + await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(`${packageJSON.repository.url}/releases/tag/v${version}`)); } } } +/** + * Registers stack analysis commands to track RHDA report generations. + * @param context - The extension context. + */ function registerStackAnalysisCommands(context: vscode.ExtensionContext) { - const invokeFullStackReport = (uri: vscode.Uri) => { - const fileUri = uri || vscode.window.activeTextEditor.document.uri; - multimanifestmodule.redhatDependencyAnalyticsReportFlow(context, fileUri); + + const invokeFullStackReport = async (uri: vscode.Uri) => { + try { + await generateRHDAReport(context, uri); + record(context, TelemetryActions.vulnerabilityReportDone, { manifest: path.basename(uri.fsPath), fileName: path.basename(uri.fsPath) }); + } catch (error) { + vscode.window.showErrorMessage(error.message); + record(context, TelemetryActions.vulnerabilityReportFailed, { manifest: path.basename(uri.fsPath), fileName: path.basename(uri.fsPath), error: error.message }); + } }; const recordAndInvoke = (origin: string, uri: vscode.Uri) => { - record(context, origin, { manifest: uri.fsPath.split('/').pop(), fileName: uri.fsPath.split('/').pop() }); - invokeFullStackReport(uri); + const fileUri = uri || vscode.window.activeTextEditor.document.uri; + record(context, origin, { manifest: fileUri.fsPath.split('/').pop(), fileName: fileUri.fsPath.split('/').pop() }); + invokeFullStackReport(fileUri); }; const registerCommand = (cmd: string, action: TelemetryActions) => { @@ -242,4 +253,4 @@ function registerStackAnalysisCommands(context: vscode.ExtensionContext) { ]; context.subscriptions.push(...stackAnalysisCommands); -} +} \ No newline at end of file diff --git a/src/multimanifestmodule.ts b/src/multimanifestmodule.ts deleted file mode 100644 index 110131217..000000000 --- a/src/multimanifestmodule.ts +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -import * as vscode from 'vscode'; -import * as path from 'path'; - -import * as stackanalysismodule from './stackanalysismodule'; -import { loadContextData } from './contextHandler'; -import { DependencyReportPanel } from './dependencyReportPanel'; - -export const redhatDependencyAnalyticsReportFlow = async (context, uri) => { - const supportedFiles = ['pom.xml', 'package.json', 'go.mod', 'requirements.txt']; - if ( - uri.fsPath && supportedFiles.includes(path.basename(uri.fsPath)) - ) { - stackanalysismodule.stackAnalysisLifeCycle( - context, - uri.fsPath - ); - } else { - vscode.window.showInformationMessage( - `File ${uri.fsPath || ''} is not supported!!` - ); - } -}; - -export const triggerManifestWs = context => { - return new Promise((resolve, reject) => { - loadContextData(context) - .then(status => { - if (status) { - DependencyReportPanel.createOrShowWebviewPanel(); - resolve(); - } - reject(`Unable to authenticate.`); - }); - }); -}; - -export const triggerTokenValidation = async (provider) => { - switch (provider) { - case 'snyk': - stackanalysismodule.validateSnykToken(); - break; - // case 'tidelift': - // add Tidelift token validation here... - // break; - // case 'sonatype': - // add Sonatype token validation here... - // break; - } -}; \ No newline at end of file diff --git a/src/redhatTelemetry.ts b/src/redhatTelemetry.ts index 6cc93f020..f61c7382a 100644 --- a/src/redhatTelemetry.ts +++ b/src/redhatTelemetry.ts @@ -1,9 +1,14 @@ import * as vscode from 'vscode'; import { getRedHatService, TelemetryEvent, TelemetryService } from '@redhat-developer/vscode-redhat-telemetry'; -export enum TelemetryActions { +/** + * Actions to be recorded for telemetry purposes. + */ +enum TelemetryActions { componentAnalysisDone = 'component_analysis_done', componentAnalysisFailed = 'component_analysis_failed', + vulnerabilityReportDone = 'vulnerability_report_done', + vulnerabilityReportFailed = 'vulnerability_report_failed', vulnerabilityReportEditor = 'vulnerability_report_editor', vulnerabilityReportExplorer = 'vulnerability_report_explorer', vulnerabilityReportPopupOpened = 'vulnerability_report_popup_opened', @@ -14,6 +19,11 @@ export enum TelemetryActions { let telemetryServiceObj: TelemetryService = null; +/** + * Retrieves the telemetry service. + * @param context The extension context. + * @returns A promise resolving to the telemetry service. + */ async function telemetryService(context: vscode.ExtensionContext): Promise { if (!telemetryServiceObj) { const redhatService = await getRedHatService(context); @@ -22,7 +32,14 @@ async function telemetryService(context: vscode.ExtensionContext): Promise((resolve, reject) => { + const reportFilePath = globalConfig.rhdaReportFilePath; + const reportDirectoryPath = path.dirname(reportFilePath); + + if (!fs.existsSync(reportDirectoryPath)) { + fs.mkdirSync(reportDirectoryPath, { recursive: true }); + } + + fs.writeFile(reportFilePath, data, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +/** + * Executes the RHDA stack analysis process. + * @param manifestFilePath The file path to the manifest file for analysis. + * @returns The stack analysis response string. + */ +async function executeStackAnalysis(manifestFilePath): Promise { + try { + return await vscode.window.withProgress({ location: vscode.ProgressLocation.Window, title: Titles.EXT_TITLE }, async p => { + return new Promise(async (resolve, reject) => { + p.report({ + message: StatusMessages.WIN_ANALYZING_DEPENDENCIES + }); + + // set up configuration options for the stack analysis request + const options = { + 'RHDA_TOKEN': globalConfig.telemetryId, + 'RHDA_SOURCE': globalConfig.utmSource, + 'MATCH_MANIFEST_VERSIONS': globalConfig.matchManifestVersions, + 'EXHORT_MVN_PATH': globalConfig.exhortMvnPath, + 'EXHORT_NPM_PATH': globalConfig.exhortNpmPath, + 'EXHORT_GO_PATH': globalConfig.exhortGoPath, + 'EXHORT_PYTHON3_PATH': globalConfig.exhortPython3Path, + 'EXHORT_PIP3_PATH': globalConfig.exhortPip3Path, + 'EXHORT_PYTHON_PATH': globalConfig.exhortPythonPath, + 'EXHORT_PIP_PATH': globalConfig.exhortPipPath + }; + + if (globalConfig.exhortSnykToken !== '') { + options['EXHORT_SNYK_TOKEN'] = globalConfig.exhortSnykToken; + } + + // execute stack analysis + await stackAnalysisService(manifestFilePath, options) + .then(async (resp) => { + p.report({ + message: StatusMessages.WIN_GENERATING_DEPENDENCIES + }); + + updateWebviewPanel(resp); + + p.report({ + message: StatusMessages.WIN_SUCCESS_DEPENDENCY_ANALYSIS + }); + + resolve(resp); + }) + .catch(err => { + p.report({ + message: StatusMessages.WIN_FAILURE_DEPENDENCY_ANALYSIS + }); + + reject(err); + }); + }); + }); + } catch (err) { + updateWebviewPanel('error'); + throw (err); + } +} + +/** + * Triggers the webview panel display. + * @param context The extension context. + * @returns A Promise that resolves once the webview panel has been triggered. + */ +async function triggerWebviewPanel(context) { + await globalConfig.authorizeRHDA(context); + DependencyReportPanel.createOrShowWebviewPanel(); +} + +/** + * Generates the RHDA report based on the provided manifest URI. + * @param context The extension context. + * @param uri The URI of the manifest file for analysis. + * @returns A promise that resolves once the report generation is complete. + */ +async function generateRHDAReport(context, uri) { + if (uri.fsPath && supportedFiles.includes(path.basename(uri.fsPath))) { + try { + + await triggerWebviewPanel(context); + const resp = await executeStackAnalysis(uri.fsPath); + if (DependencyReportPanel.currentPanel) { + await writeReportToFile(resp); + } + + } catch (error) { + throw (error); + } + } else { + vscode.window.showInformationMessage( + `File ${uri.fsPath} is not supported!!` + ); + } +} + +export { generateRHDAReport }; \ No newline at end of file diff --git a/src/stackanalysismodule.ts b/src/stackanalysismodule.ts deleted file mode 100644 index f12559f46..000000000 --- a/src/stackanalysismodule.ts +++ /dev/null @@ -1,119 +0,0 @@ -'use strict'; - -import * as vscode from 'vscode'; -import * as path from 'path'; -import * as fs from 'fs'; - -import * as config from './config'; -import { snykURL, defaultRedhatDependencyAnalyticsReportFilePath, StatusMessages, Titles } from './constants'; -import * as multimanifestmodule from './multimanifestmodule'; -import * as stackAnalysisServices from './stackAnalysisService'; -import { DependencyReportPanel } from './dependencyReportPanel'; - -export const stackAnalysisLifeCycle = ( - context, - manifestFilePath, -) => { - vscode.window.withProgress( - { - location: vscode.ProgressLocation.Window, - title: Titles.EXT_TITLE - }, - p => { - return new Promise(async (resolve, reject) => { - - // get config data from extension workspace setting - const apiConfig = config.getApiConfig(); - - // create webview panel - await multimanifestmodule.triggerManifestWs(context); - p.report({ - message: StatusMessages.WIN_ANALYZING_DEPENDENCIES - }); - - // set up configuration options for the stack analysis request - const options = { - 'RHDA_TOKEN': process.env.VSCEXT_TELEMETRY_ID, - 'RHDA_SOURCE': process.env.VSCEXT_UTM_SOURCE, - 'EXHORT_DEV_MODE': process.env.VSCEXT_EXHORT_DEV_MODE, - 'MATCH_MANIFEST_VERSIONS': apiConfig.matchManifestVersions ? 'true' : 'false', - 'EXHORT_MVN_PATH': config.getMvnExecutable(), - 'EXHORT_NPM_PATH': config.getNpmExecutable(), - 'EXHORT_GO_PATH': config.getGoExecutable(), - 'EXHORT_PYTHON3_PATH': config.getPython3Executable(), - 'EXHORT_PIP3_PATH': config.getPip3Executable(), - 'EXHORT_PYTHON_PATH': config.getPythonExecutable(), - 'EXHORT_PIP_PATH': config.getPipExecutable() - }; - - if (apiConfig.exhortSnykToken !== '') { - options['EXHORT_SNYK_TOKEN'] = apiConfig.exhortSnykToken; - } - - // execute stack analysis - stackAnalysisServices.exhortApiStackAnalysis(manifestFilePath, options) - .then(resp => { - p.report({ - message: StatusMessages.WIN_GENERATING_DEPENDENCIES - }); - const reportFilePath = apiConfig.redHatDependencyAnalyticsReportFilePath || defaultRedhatDependencyAnalyticsReportFilePath; - const reportDirectoryPath = path.dirname(reportFilePath); - if (!fs.existsSync(reportDirectoryPath)) { - fs.mkdirSync(reportDirectoryPath, { recursive: true }); - } - fs.writeFile(reportFilePath, resp, (err) => { - if (err) { - reject(err); - } else { - if (DependencyReportPanel.currentPanel) { - DependencyReportPanel.currentPanel.doUpdatePanel(resp); - } - p.report({ - message: StatusMessages.WIN_SUCCESS_DEPENDENCY_ANALYSIS - }); - resolve(null); - } - }); - }) - .catch(err => { - p.report({ - message: StatusMessages.WIN_FAILURE_DEPENDENCY_ANALYSIS - }); - handleError(err); - reject(); - }); - }); - } - ); -}; - -export const handleError = err => { - if (DependencyReportPanel.currentPanel) { - DependencyReportPanel.currentPanel.doUpdatePanel('error'); - } - vscode.window.showErrorMessage(err.message); -}; - -export const validateSnykToken = async () => { - const apiConfig = config.getApiConfig(); - if (apiConfig.exhortSnykToken !== '') { - - // set up configuration options for the token validation request - const options = { - 'RHDA_TOKEN': process.env.VSCEXT_TELEMETRY_ID, - 'RHDA_SOURCE': process.env.VSCEXT_UTM_SOURCE, - 'EXHORT_DEV_MODE': process.env.VSCEXT_EXHORT_DEV_MODE, - 'EXHORT_SNYK_TOKEN': apiConfig.exhortSnykToken - }; - - // execute stack analysis - stackAnalysisServices.getSnykTokenValidationService(options); - - } else { - - vscode.window.showInformationMessage(`Please note that if you fail to provide a valid Snyk Token in the extension workspace settings, - Snyk vulnerabilities will not be displayed. - To resolve this issue, please obtain a valid token from the following link: [here](${snykURL}).`); - - } -}; diff --git a/src/template.ts b/src/template.ts index bd0deefcf..29e9d22fa 100644 --- a/src/template.ts +++ b/src/template.ts @@ -3,7 +3,7 @@ import { Titles } from './constants'; /** - * Commonly used templates + * HTML template for the loader screen during RHDA analysis. */ export const LOADER_TEMPLATE = ` @@ -259,6 +259,9 @@ export const LOADER_TEMPLATE = ` `; +/** + * HTML template for displaying an error message when unable to analyze the stack. + */ export const ERROR_TEMPLATE = ` diff --git a/src/tokenValidation.ts b/src/tokenValidation.ts new file mode 100644 index 000000000..6dc1452fc --- /dev/null +++ b/src/tokenValidation.ts @@ -0,0 +1,35 @@ +'use strict'; + +import * as vscode from 'vscode'; + +import { globalConfig } from './config'; +import { snykURL } from './constants'; +import { tokenValidationService } from './exhortServices'; + +/** + * Validates the Snyk token using the Exhort token validation service. + * @returns A Promise that resolves when token has been validated. + */ +async function validateSnykToken() { + if (globalConfig.exhortSnykToken !== '') { + + // set up configuration options for the token validation request + const options = { + 'RHDA_TOKEN': globalConfig.telemetryId, + 'RHDA_SOURCE': globalConfig.utmSource, + 'EXHORT_SNYK_TOKEN': globalConfig.exhortSnykToken + }; + + // execute token validation + tokenValidationService(options, 'Snyk'); + + } else { + + vscode.window.showInformationMessage(`Please note that if you fail to provide a valid Snyk Token in the extension workspace settings, + Snyk vulnerabilities will not be displayed. + To resolve this issue, please obtain a valid token from the following link: [here](${snykURL}).`); + + } +} + +export { validateSnykToken }; diff --git a/test/caNotification.test.ts b/test/caNotification.test.ts new file mode 100644 index 000000000..261300f0f --- /dev/null +++ b/test/caNotification.test.ts @@ -0,0 +1,166 @@ +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; + +import { CANotification } from '../src/caNotification'; + +const expect = chai.expect; +chai.use(sinonChai); + +suite('CANotification module', () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('should create an instance with default values when no data is provided', async () => { + const notification = new CANotification({ + errorMessage: null, + done: null, + uri: '', + diagCount: null, + vulnCount: null, + }); + + expect(notification.errorMsg()).to.equal(''); + expect(notification.origin()).to.equal(''); + expect(notification.isDone()).to.be.false; + expect(notification.hasWarning()).to.be.false; + expect(notification.popupText()).to.equal(' no vulnerabilities found for all the providers combined'); + expect(notification.statusText()).to.equal('$(sync~spin) RHDA analysis in progress'); + }); + + + test('should create an instance with provided data when CA has been initiated', async () => { + const notification = new CANotification({ + errorMessage: null, + done: false, + uri: 'file:///mock/path', + diagCount: null, + vulnCount: null, + }); + + expect(notification.errorMsg()).to.equal(''); + expect(notification.origin()).to.equal('file:///mock/path'); + expect(notification.isDone()).to.be.false; + expect(notification.hasWarning()).to.be.false; + expect(notification.popupText()).to.equal(' no vulnerabilities found for all the providers combined'); + expect(notification.statusText()).to.equal('$(sync~spin) RHDA analysis in progress'); + }); + + test('should create an instance with provided data when CA has completed successfully with one vulnerability from one vulnerability provider', async () => { + const mockVulnCountMap = new Map(); + mockVulnCountMap['snyk'] = 1; + const notification = new CANotification({ + errorMessage: null, + done: true, + uri: 'file:///mock/path', + diagCount: 1, + vulnCount: mockVulnCountMap, + }); + + expect(notification.errorMsg()).to.equal(''); + expect(notification.origin()).to.equal('file:///mock/path'); + expect(notification.isDone()).to.be.true; + expect(notification.hasWarning()).to.be.true; + expect(notification.popupText()).to.equal('Found 1 direct vulnerability for Snyk Provider.'); + expect(notification.statusText()).to.equal('$(warning) 1 direct vulnerability found for all the providers combined'); + }); + + test('should create an instance with provided data when CA has completed successfully with many vulnerabilities from one vulnerability provider', async () => { + const mockVulnCountMap = new Map(); + mockVulnCountMap['snyk'] = 3; + const notification = new CANotification({ + errorMessage: null, + done: true, + uri: 'file:///mock/path', + diagCount: 2, + vulnCount: mockVulnCountMap, + }); + + expect(notification.errorMsg()).to.equal(''); + expect(notification.origin()).to.equal('file:///mock/path'); + expect(notification.isDone()).to.be.true; + expect(notification.hasWarning()).to.be.true; + expect(notification.popupText()).to.equal('Found 3 direct vulnerabilities for Snyk Provider.'); + expect(notification.statusText()).to.equal('$(warning) 3 direct vulnerabilities found for all the providers combined'); + }); + + test('should create an instance with provided data when CA has completed successfully with vulnerabilities from multiple vulnerability providers', async () => { + const mockVulnCountMap = new Map(); + mockVulnCountMap['snyk'] = 3; + mockVulnCountMap['oss-index'] = 1; + const notification = new CANotification({ + errorMessage: null, + done: true, + uri: 'file:///mock/path', + diagCount: 2, + vulnCount: mockVulnCountMap, + }); + + expect(notification.errorMsg()).to.equal(''); + expect(notification.origin()).to.equal('file:///mock/path'); + expect(notification.isDone()).to.be.true; + expect(notification.hasWarning()).to.be.true; + expect(notification.popupText()).to.equal('Found 3 direct vulnerabilities for Snyk Provider. Found 1 direct vulnerability for Oss-Index Provider.'); + expect(notification.statusText()).to.equal('$(warning) 4 direct vulnerabilities found for all the providers combined'); + }); + + test('should create an instance with provided data when CA has completed successfully with no vulnerabilities', async () => { + const mockVulnCountMap = new Map(); + const notification = new CANotification({ + errorMessage: null, + done: true, + uri: 'file:///mock/path', + diagCount: 0, + vulnCount: mockVulnCountMap, + }); + + expect(notification.errorMsg()).to.equal(''); + expect(notification.origin()).to.equal('file:///mock/path'); + expect(notification.isDone()).to.be.true; + expect(notification.hasWarning()).to.be.false; + expect(notification.popupText()).to.equal(' no vulnerabilities found for all the providers combined'); + expect(notification.statusText()).to.equal('$(shield)$(check)'); + }); + + test('should create an instance with provided data when CA has completed successfully with diagnostic but no vulnerabilities', async () => { + const mockVulnCountMap = new Map(); + const notification = new CANotification({ + errorMessage: null, + done: true, + uri: 'file:///mock/path', + diagCount: 1, + vulnCount: mockVulnCountMap, + }); + + expect(notification.errorMsg()).to.equal(''); + expect(notification.origin()).to.equal('file:///mock/path'); + expect(notification.isDone()).to.be.true; + expect(notification.hasWarning()).to.be.true; + expect(notification.popupText()).to.equal(' no vulnerabilities found for all the providers combined'); + expect(notification.statusText()).to.equal('$(warning) no vulnerabilities found for all the providers combined'); + }); + + test('should create an instance with provided data when CA has failed with error', async () => { + const notification = new CANotification({ + errorMessage: 'Mock error message', + done: null, + uri: 'file:///mock/path', + diagCount: null, + vulnCount: null, + }); + + expect(notification.errorMsg()).to.equal('Mock error message'); + expect(notification.origin()).to.equal('file:///mock/path'); + expect(notification.isDone()).to.be.false; + expect(notification.hasWarning()).to.be.false; + expect(notification.popupText()).to.equal(' no vulnerabilities found for all the providers combined'); + expect(notification.statusText()).to.equal('$(sync~spin) RHDA analysis in progress'); + }); +}); diff --git a/test/caStatusBarProvider.test.ts b/test/caStatusBarProvider.test.ts new file mode 100644 index 000000000..d381a8911 --- /dev/null +++ b/test/caStatusBarProvider.test.ts @@ -0,0 +1,57 @@ +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as vscode from 'vscode'; + +import { caStatusBarProvider } from '../src/caStatusBarProvider'; +import { PromptText } from '../src/constants'; +import * as commands from '../src/commands'; + +const expect = chai.expect; +chai.use(sinonChai); + +suite('CAStatusBarProvider module', () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('should show summary with provided text and URI', async () => { + const text = 'Mock Text'; + const uri = 'file:///mock/path'; + + caStatusBarProvider.showSummary(text, uri); + console.log(caStatusBarProvider['statusBarItem'].command); + + expect(caStatusBarProvider['statusBarItem'].text).to.equal(text); + expect(caStatusBarProvider['statusBarItem'].tooltip).to.equal(PromptText.FULL_STACK_PROMPT_TEXT); + expect(caStatusBarProvider['statusBarItem'].command).to.deep.equal({ + title: PromptText.FULL_STACK_PROMPT_TEXT, + command: commands.TRIGGER_FULL_STACK_ANALYSIS_FROM_STATUS_BAR, + arguments: [vscode.Uri.parse(uri)] + }); + }); + + test('should set error message and command', () => { + caStatusBarProvider.setError(); + + expect(caStatusBarProvider['statusBarItem'].text).to.equal('$(error) RHDA analysis has failed'); + expect(caStatusBarProvider['statusBarItem'].command).to.deep.equal({ + title: PromptText.LSP_FAILURE_TEXT, + command: commands.TRIGGER_STACK_LOGS, + }); + }); + + test('should dispose status bar item', () => { + const disposeStub = sandbox.stub(caStatusBarProvider['statusBarItem'], 'dispose'); + + caStatusBarProvider.dispose(); + + expect(disposeStub.calledOnce).to.be.true; + }); +}); \ No newline at end of file diff --git a/test/config.test.ts b/test/config.test.ts index 988ef67d4..4eed096d6 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -2,8 +2,11 @@ import * as chai from 'chai'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; -import * as vscode from 'vscode'; -import * as Config from '../src/config'; +import { globalConfig } from '../src/config'; +import { GlobalState } from '../src/constants'; +import * as commands from '../src/commands'; +import * as redhatTelemetry from '../src/redhatTelemetry'; +import { context } from './vscontext.mock'; const expect = chai.expect; chai.use(sinonChai); @@ -19,166 +22,42 @@ suite('Config module', () => { sandbox.restore(); }); - test('getApiConfig should get API config', async () => { - const getConfigurationStub = sandbox.stub(vscode.workspace, 'getConfiguration'); - getConfigurationStub.withArgs('redHatDependencyAnalytics').resolves('mockApiConfig'); - - const apiConfig = await Config.getApiConfig(); - - expect(apiConfig).to.equal('mockApiConfig'); - }); - - test('getMvnExecutable should get default mvn executable', () => { - let mvnPath = Config.getMvnExecutable(); - - expect(mvnPath).equals('mvn'); - }); - - test('getMvnExecutable should get custom mvn executable', () => { - const getConfigurationStub = sandbox.stub(vscode.workspace, 'getConfiguration'); - const configMock = { - get: (key: string) => { - if (key === 'path') { - return 'path/to/mvn'; - } - }, - }; - getConfigurationStub.withArgs('mvn.executable').returns(configMock as vscode.WorkspaceConfiguration); - - let mvnPath = Config.getMvnExecutable(); - - expect(mvnPath).equals('path/to/mvn'); - }); - - test('getNpmExecutable should get default npm executable', () => { - let npmPath = Config.getNpmExecutable(); - - expect(npmPath).equals('npm'); - }); - - test('getNpmExecutable should get custom npm executable', () => { - const getConfigurationStub = sandbox.stub(vscode.workspace, 'getConfiguration'); - const configMock = { - get: (key: string) => { - if (key === 'path') { - return 'path/to/npm'; - } - }, - }; - getConfigurationStub.withArgs('npm.executable').returns(configMock as vscode.WorkspaceConfiguration); - - let npmPath = Config.getNpmExecutable(); - - expect(npmPath).equals('path/to/npm'); - }); - - test('getGoExecutable should get default go executable', () => { - let goPath = Config.getGoExecutable(); - - expect(goPath).equals('go'); - }); - - test('getGoExecutable should get custom go executable', () => { - const getConfigurationStub = sandbox.stub(vscode.workspace, 'getConfiguration'); - const configMock = { - get: (key: string) => { - if (key === 'path') { - return 'path/to/go'; - } - }, - }; - getConfigurationStub.withArgs('go.executable').returns(configMock as vscode.WorkspaceConfiguration); - - let goPath = Config.getGoExecutable(); - - expect(goPath).equals('path/to/go'); - }); - - test('getPython3Executable should get default python3 executable', () => { - let python3Path = Config.getPython3Executable(); - - expect(python3Path).equals('python3'); - }); - - test('getPython3Executable should get custom python3 executable', () => { - const getConfigurationStub = sandbox.stub(vscode.workspace, 'getConfiguration'); - const configMock = { - get: (key: string) => { - if (key === 'path') { - return 'path/to/python3'; - } - }, - }; - getConfigurationStub.withArgs('python3.executable').returns(configMock as vscode.WorkspaceConfiguration); - - let python3Path = Config.getPython3Executable(); - - expect(python3Path).equals('path/to/python3'); - }); - - test('getPip3Executable should get default pip3 executable', () => { - let pip3Path = Config.getPip3Executable(); - - expect(pip3Path).equals('pip3'); - }); - - test('getPip3Executable should get custom pip3 executable', () => { - const getConfigurationStub = sandbox.stub(vscode.workspace, 'getConfiguration'); - const configMock = { - get: (key: string) => { - if (key === 'path') { - return 'path/to/pip3'; - } - }, - }; - getConfigurationStub.withArgs('pip3.executable').returns(configMock as vscode.WorkspaceConfiguration); - - let pip3Path = Config.getPip3Executable(); - - expect(pip3Path).equals('path/to/pip3'); - }); - - test('getPythonExecutable should get default python executable', () => { - let pythonPath = Config.getPythonExecutable(); - - expect(pythonPath).equals('python'); - }); - - test('getPythonExecutable should get custom python executable', () => { - const getConfigurationStub = sandbox.stub(vscode.workspace, 'getConfiguration'); - const configMock = { - get: (key: string) => { - if (key === 'path') { - return 'path/to/python'; - } - }, - }; - getConfigurationStub.withArgs('python.executable').returns(configMock as vscode.WorkspaceConfiguration); - - let pythonPath = Config.getPythonExecutable(); - - expect(pythonPath).equals('path/to/python'); - }); - - test('getPipExecutable should get default pip executable', () => { - let pipPath = Config.getPipExecutable(); - - expect(pipPath).equals('pip'); - }); - - test('getPipExecutable should get custom pip executable', () => { - const getConfigurationStub = sandbox.stub(vscode.workspace, 'getConfiguration'); - const configMock = { - get: (key: string) => { - if (key === 'path') { - return 'path/to/pip'; - } - }, - }; - getConfigurationStub.withArgs('pip.executable').returns(configMock as vscode.WorkspaceConfiguration); - - let pipPath = Config.getPipExecutable(); - - expect(pipPath).equals('path/to/pip'); + test('should initialize Config properties with default extension settings', async () => { + + expect(globalConfig.triggerFullStackAnalysis).to.eq(commands.TRIGGER_FULL_STACK_ANALYSIS); + expect(globalConfig.triggerRHRepositoryRecommendationNotification).to.eq(commands.TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION); + expect(globalConfig.utmSource).to.eq(GlobalState.UTM_SOURCE); + expect(globalConfig.exhortSnykToken).to.eq(''); + expect(globalConfig.matchManifestVersions).to.eq('true'); + expect(globalConfig.rhdaReportFilePath).to.eq('/tmp/redhatDependencyAnalyticsReport.html'); + expect(globalConfig.exhortMvnPath).to.eq('mvn'); + expect(globalConfig.exhortNpmPath).to.eq('npm'); + expect(globalConfig.exhortGoPath).to.eq('go'); + expect(globalConfig.exhortPython3Path).to.eq('python3'); + expect(globalConfig.exhortPip3Path).to.eq('pip3'); + expect(globalConfig.exhortPythonPath).to.eq('python'); + expect(globalConfig.exhortPipPath).to.eq('pip'); + + expect(process.env['VSCEXT_TRIGGER_FULL_STACK_ANALYSIS']).to.eq(commands.TRIGGER_FULL_STACK_ANALYSIS); + expect(process.env['VSCEXT_TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION']).to.eq(commands.TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION); + expect(process.env['VSCEXT_UTM_SOURCE']).to.eq(GlobalState.UTM_SOURCE); + expect(process.env['VSCEXT_EXHORT_SNYK_TOKEN']).to.eq(''); + expect(process.env['VSCEXT_MATCH_MANIFEST_VERSIONS']).to.eq('true'); + expect(process.env['VSCEXT_EXHORT_MVN_PATH']).to.eq('mvn'); + expect(process.env['VSCEXT_EXHORT_NPM_PATH']).to.eq('npm'); + expect(process.env['VSCEXT_EXHORT_GO_PATH']).to.eq('go'); + expect(process.env['VSCEXT_EXHORT_PYTHON3_PATH']).to.eq('python3'); + expect(process.env['VSCEXT_EXHORT_PIP3_PATH']).to.eq('pip3'); + expect(process.env['VSCEXT_EXHORT_PYTHON_PATH']).to.eq('python'); + expect(process.env['VSCEXT_EXHORT_PIP_PATH']).to.eq('pip'); + }); + + test('should call retrieve telemetry parameters from getTelemetryId', async () => { + sandbox.stub(redhatTelemetry, 'getTelemetryId').resolves('mockId'); + + await globalConfig.authorizeRHDA(context); + + expect(globalConfig.telemetryId).to.equal('mockId'); + expect(process.env['VSCEXT_TELEMETRY_ID']).to.equal('mockId'); }); }); diff --git a/test/contextHandler.test.ts b/test/contextHandler.test.ts deleted file mode 100644 index c4b9af7e9..000000000 --- a/test/contextHandler.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as chai from 'chai'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -import { loadEnvironmentData } from '../src/contextHandler'; -import { GlobalState } from '../src/constants'; - -const expect = chai.expect; -chai.use(sinonChai); - -suite('contextHandler Modules', () => { - let sandbox: sinon.SinonSandbox; - - setup(() => { - sandbox = sinon.createSandbox(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('setContextData should set environment variables', async () => { - - loadEnvironmentData(); - - expect(process.env['VSCEXT_PROVIDE_FULLSTACK_ACTION']).equals('true'); - expect(process.env['VSCEXT_UTM_SOURCE']).equals(GlobalState.UTM_SOURCE); - expect(process.env['VSCEXT_EXHORT_DEV_MODE']).equals(GlobalState.EXHORT_DEV_MODE); - expect(process.env['VSCEXT_EXHORT_SNYK_TOKEN']).equals(''); - expect(process.env['VSCEXT_MATCH_MANIFEST_VERSIONS']).equals('true'); - expect(process.env['VSCEXT_EXHORT_MVN_PATH']).equals('mvn'); - expect(process.env['VSCEXT_EXHORT_NPM_PATH']).equals('npm'); - expect(process.env['VSCEXT_EXHORT_GO_PATH']).equals('go'); - expect(process.env['VSCEXT_EXHORT_PYTHON3_PATH']).equals('python3'); - expect(process.env['VSCEXT_EXHORT_PIP3_PATH']).equals('pip3'); - expect(process.env['VSCEXT_EXHORT_PYTHON_PATH']).equals('python'); - expect(process.env['VSCEXT_EXHORT_PIP_PATH']).equals('pip'); - }); -}); diff --git a/test/depOutputChannel.test.ts b/test/depOutputChannel.test.ts index 9c6ef5c0d..c9a19ad7c 100644 --- a/test/depOutputChannel.test.ts +++ b/test/depOutputChannel.test.ts @@ -2,7 +2,7 @@ import * as chai from 'chai'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; -import { DepOutputChannel } from '../src/DepOutputChannel'; +import { DepOutputChannel } from '../src/depOutputChannel'; import { Titles } from '../src/constants'; const expect = chai.expect; diff --git a/test/dependencyReportPanel.test.ts b/test/dependencyReportPanel.test.ts index ea59968d4..a1df5b3fe 100644 --- a/test/dependencyReportPanel.test.ts +++ b/test/dependencyReportPanel.test.ts @@ -4,10 +4,9 @@ import * as sinonChai from 'sinon-chai'; import * as vscode from 'vscode'; import * as fs from 'fs'; -import * as Config from '../src/config'; import { DependencyReportPanel } from '../src/dependencyReportPanel'; import * as Templates from '../src/template'; -import { defaultRedhatDependencyAnalyticsReportFilePath } from '../src/constants'; +import { defaultRhdaReportFilePath } from '../src/constants'; const expect = chai.expect; chai.use(sinonChai); @@ -81,33 +80,14 @@ suite('DependencyReportPanel Modules', () => { }); test('dispose should dispose of current panel with RHDA report path setting', async () => { - sandbox.stub(Config, 'getApiConfig').returns({ - redHatDependencyAnalyticsReportFilePath: 'mockFilePath', - }); - const existsSyncStub = sandbox.stub(fs, 'existsSync').returns(true); - const unlinkSyncStub = sandbox.stub(fs, 'unlinkSync'); - - DependencyReportPanel.currentPanel.dispose(); - - expect(existsSyncStub).to.be.calledWith('mockFilePath'); - expect(unlinkSyncStub).to.be.calledWith('mockFilePath'); - expect(DependencyReportPanel.data).equals(null); - expect(DependencyReportPanel.currentPanel).equals(undefined); - }); - test('dispose should dispose of current panel with default RHDA report path', async () => { - sandbox.stub(Config, 'getApiConfig').returns({ - redHatDependencyAnalyticsReportFilePath: '', - }); const existsSyncStub = sandbox.stub(fs, 'existsSync').returns(true); const unlinkSyncStub = sandbox.stub(fs, 'unlinkSync'); - DependencyReportPanel.createOrShowWebviewPanel(); - DependencyReportPanel.currentPanel.dispose(); - expect(existsSyncStub).to.be.calledWith(defaultRedhatDependencyAnalyticsReportFilePath); - expect(unlinkSyncStub).to.be.calledWith(defaultRedhatDependencyAnalyticsReportFilePath); + expect(existsSyncStub).to.be.calledWith(defaultRhdaReportFilePath); + expect(unlinkSyncStub).to.be.calledWith(defaultRhdaReportFilePath); expect(DependencyReportPanel.data).equals(null); expect(DependencyReportPanel.currentPanel).equals(undefined); }); diff --git a/test/extension.test.ts b/test/extension.test.ts index 76127956d..32b436cd6 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -22,27 +22,9 @@ suite('Fabric8 Analytics Extension', () => { Commands.TRIGGER_FULL_STACK_ANALYSIS_FROM_EDITOR, Commands.TRIGGER_FULL_STACK_ANALYSIS_FROM_EXPLORER, Commands.TRIGGER_FULL_STACK_ANALYSIS_FROM_PIE_BTN, - Commands.TRIGGER_FULL_STACK_ANALYSIS_FROM_STATUS_BAR, - Commands.TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION + Commands.TRIGGER_FULL_STACK_ANALYSIS_FROM_STATUS_BAR ]; // @ts-ignore assert.ok((await vscode.commands.getCommands(true)).includes(...FABRIC8_COMMANDS)); }); - - test('should trigger fabric8-analytics full stack-report activate', async () => { - await vscode.commands - .executeCommand(Commands.TRIGGER_FULL_STACK_ANALYSIS) - .then( - (res) => { - assert.ok(true); - }, - (reason: any) => { - assert.equal(reason.name, 'Error'); - assert.equal( - reason.message, - `Running the contributed command: '${Commands.TRIGGER_FULL_STACK_ANALYSIS}' failed.` - ); - } - ); - }); }); diff --git a/test/multiManifestModule.test.ts b/test/multiManifestModule.test.ts deleted file mode 100644 index 30d22ba57..000000000 --- a/test/multiManifestModule.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import * as chai from 'chai'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; -import * as vscode from 'vscode'; - -import { context } from './vscontext.mock'; -import * as multimanifestmodule from '../src/multimanifestmodule'; -import * as contextHandler from '../src/contextHandler'; -import * as stackanalysismodule from '../src/stackanalysismodule'; -import { DependencyReportPanel } from '../src/dependencyReportPanel'; - -const expect = chai.expect; -chai.use(sinonChai); - -suite('multimanifest module', () => { - let sandbox: sinon.SinonSandbox; - - setup(() => { - sandbox = sinon.createSandbox(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('redhatDependencyAnalyticsReportFlow should process stack analysis for maven when given a pom.xml', async () => { - const uri = vscode.Uri.file('path/to/pom.xml'); - const stackAnalysisLifeCycleStub = sandbox.stub(stackanalysismodule, 'stackAnalysisLifeCycle'); - - await multimanifestmodule.redhatDependencyAnalyticsReportFlow(context, uri); - - expect(stackAnalysisLifeCycleStub.calledOnceWithExactly(context, uri.fsPath)).to.be.true; - }); - - test('redhatDependencyAnalyticsReportFlow should show an information message for an unsupported file', async () => { - const showInformationMessageSpy = sandbox.spy(vscode.window, 'showInformationMessage'); - - const uri = vscode.Uri.file('path/to/unsupported.txt'); - - await multimanifestmodule.redhatDependencyAnalyticsReportFlow(context, uri); - - expect(showInformationMessageSpy).to.be.calledWith('File /path/to/unsupported.txt is not supported!!'); - }); - - test('triggerManifestWs should resolve with true when authorized and create DependencyReportPanel', async () => { - let loadContextDataStub = sandbox.stub(contextHandler, 'loadContextData').resolves(true); - const createOrShowWebviewPanelStub = sandbox.stub(DependencyReportPanel, 'createOrShowWebviewPanel'); - - try { - await multimanifestmodule.triggerManifestWs(context); - // If triggerManifestWs resolves successfully, the test will pass. - } catch (error) { - // If triggerManifestWs rejects, the test will fail with the error message. - expect.fail('Expected triggerManifestWs to resolve, but it rejected with an error: ' + error); - } - - expect(loadContextDataStub.calledOnce).to.be.true; - expect(createOrShowWebviewPanelStub.calledOnce).to.be.true; - }); - - test('triggerManifestWs should reject with "Unable to authenticate." when authorization fails', async () => { - let loadContextDataStub = sandbox.stub(contextHandler, 'loadContextData').resolves(false); - const createOrShowWebviewPanelStub = sandbox.stub(DependencyReportPanel, 'createOrShowWebviewPanel'); - - try { - await multimanifestmodule.triggerManifestWs(context); - // The test should not reach this point, so fail if it does - expect.fail('Function should have rejected'); - } catch (error) { - expect(error).to.equal('Unable to authenticate.'); - } - - expect(loadContextDataStub.calledOnce).to.be.true; - expect(createOrShowWebviewPanelStub.called).to.be.false; - }); - - test('triggerTokenValidation should call validateSnykToken when provider is "snyk"', async () => { - const validateSnykTokenStub = sandbox.stub(stackanalysismodule, 'validateSnykToken'); - - await multimanifestmodule.triggerTokenValidation('snyk'); - - expect(validateSnykTokenStub.calledOnce).to.be.true; - }); - - test('triggerTokenValidation should end when undefined provider is called', async () => { - const validateSnykTokenStub = sandbox.stub(stackanalysismodule, 'validateSnykToken'); - - await multimanifestmodule.triggerTokenValidation('undefined'); - - expect(validateSnykTokenStub.called).to.be.false; - }); - -}); diff --git a/test/stackAnalysis.test.ts b/test/stackAnalysis.test.ts new file mode 100644 index 000000000..b140a6cfe --- /dev/null +++ b/test/stackAnalysis.test.ts @@ -0,0 +1,120 @@ +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as vscode from 'vscode'; +import * as fs from 'fs'; + +import * as exhortServices from '../src/exhortServices'; +import { globalConfig } from '../src/config'; +import { DependencyReportPanel } from '../src/dependencyReportPanel'; +import { generateRHDAReport } from '../src/stackAnalysis' +import { context } from './vscontext.mock'; +import * as templates from '../src/template'; + +const expect = chai.expect; +chai.use(sinonChai); + +suite('StackAnalysis module', () => { + let sandbox: sinon.SinonSandbox; + const MockUri = vscode.Uri.file('/mock/path/pom.xml'); + const mockReponse = ' mockResponse '; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('should ignore unsoported file', async () => { + const unsupportedUri = vscode.Uri.file('/mock/path/yarn.lock'); + const authorizeRHDAStub = sandbox.stub(globalConfig, 'authorizeRHDA').resolves(); + const stackAnalysisServiceStub = sandbox.stub(exhortServices, 'stackAnalysisService').resolves(mockReponse) + const showInformationMessageSpy = sandbox.spy(vscode.window, 'showInformationMessage'); + + await generateRHDAReport(context, unsupportedUri); + + expect(authorizeRHDAStub.calledOnce).to.be.false; + expect(stackAnalysisServiceStub.calledOnce).to.be.false; + expect(showInformationMessageSpy.calledOnceWith(`File ${unsupportedUri.fsPath} is not supported!!`)).to.be.true; + }); + + test('should generate RHDA report for supported file and successfully save HTML data locally', async () => { + const authorizeRHDAStub = sandbox.stub(globalConfig, 'authorizeRHDA').resolves(); + const stackAnalysisServiceStub = sandbox.stub(exhortServices, 'stackAnalysisService').resolves(mockReponse) + const existsSyncStub = sandbox.stub(fs, 'existsSync').returns(true); + const writeFileStub = sandbox.stub(fs, 'writeFile').callsFake((path, data, callback) => { + callback(null); + }); + + globalConfig.exhortSnykToken = 'mockToken'; + + await generateRHDAReport(context, MockUri); + + expect(authorizeRHDAStub.calledOnce).to.be.true; + expect(stackAnalysisServiceStub.calledOnce).to.be.true; + expect(existsSyncStub.calledOnce).to.be.true; + expect(writeFileStub.calledWithMatch('/tmp/redhatDependencyAnalyticsReport.html', 'mockResponse')).to.be.true; + expect(DependencyReportPanel.data).to.eq(mockReponse); + }); + + test('should fail to generate RHDA report for supported file', async () => { + const authorizeRHDAStub = sandbox.stub(globalConfig, 'authorizeRHDA').resolves(); + const stackAnalysisServiceStub = sandbox.stub(exhortServices, 'stackAnalysisService').rejects(new Error('Mock Error')); + + globalConfig.exhortSnykToken = ''; + + await generateRHDAReport(context, MockUri) + .then(() => { + throw (new Error('should have thrown error')) + }) + .catch(error => { + expect(error.message).to.eq('Mock Error'); + expect(authorizeRHDAStub.calledOnce).to.be.true; + expect(stackAnalysisServiceStub.calledOnce).to.be.true; + expect(DependencyReportPanel.data).to.eq(templates.ERROR_TEMPLATE); + }) + }); + + test('should generate RHDA report for supported file successfully but fail to save HTML locally', async () => { + const authorizeRHDAStub = sandbox.stub(globalConfig, 'authorizeRHDA').resolves(); + const stackAnalysisServiceStub = sandbox.stub(exhortServices, 'stackAnalysisService').resolves(mockReponse) + sandbox.stub(fs, 'existsSync').returns(false); + const writeFileStub = sandbox.stub(fs, 'writeFile').callsFake((path, data, callback) => { + callback(new Error('Mock Error')); + }); + + await generateRHDAReport(context, MockUri) + .then(() => { + throw (new Error('should have thrown error')) + }) + .catch(error => { + expect(error.message).to.eq('Mock Error'); + expect(authorizeRHDAStub.calledOnce).to.be.true; + expect(stackAnalysisServiceStub.calledOnce).to.be.true; + expect(writeFileStub.calledOnce).to.be.true; + expect(DependencyReportPanel.data).to.eq(mockReponse); + }) + }); + + test('should generate RHDA report for supported file successfully but fail to create directory to save HTML locally', async () => { + const authorizeRHDAStub = sandbox.stub(globalConfig, 'authorizeRHDA').resolves(); + const stackAnalysisServiceStub = sandbox.stub(exhortServices, 'stackAnalysisService').resolves(mockReponse) + sandbox.stub(fs, 'existsSync').returns(false); + const mkdirSyncStub = sandbox.stub(fs, 'mkdirSync').throws(new Error('Mock Error')); + + await generateRHDAReport(context, MockUri) + .then(() => { + throw (new Error('should have thrown error')) + + }) + .catch(error => { + expect(error.message).to.eq('Mock Error'); + expect(authorizeRHDAStub.calledOnce).to.be.true; + expect(stackAnalysisServiceStub.calledOnce).to.be.true; + expect(mkdirSyncStub.calledOnce).to.be.true; + expect(DependencyReportPanel.data).to.eq(mockReponse); + }) + }); +}); diff --git a/test/stackAnalysisModule.test.ts b/test/stackAnalysisModule.test.ts deleted file mode 100644 index 3dd36e3ce..000000000 --- a/test/stackAnalysisModule.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as chai from 'chai'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; -import * as vscode from 'vscode'; - -import { context } from './vscontext.mock'; -import * as stackanalysismodule from '../src/stackanalysismodule'; -import * as multimanifestmodule from '../src/multimanifestmodule'; -import * as stackAnalysisServices from '../src/stackAnalysisService'; -import * as Config from '../src/config'; - -const expect = chai.expect; -chai.use(sinonChai); - -suite('stackanalysis module', () => { - let sandbox: sinon.SinonSandbox; - - setup(() => { - sandbox = sinon.createSandbox(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('stackAnalysisLifeCycle should call chain of promises', async () => { - const withProgressSpy = sandbox.spy(vscode.window, 'withProgress'); - const triggerManifestWsStub = sandbox.stub(multimanifestmodule, 'triggerManifestWs'); - const exhortApiStackAnalysisStub = sandbox.stub(stackAnalysisServices, 'exhortApiStackAnalysis'); - - await stackanalysismodule.stackAnalysisLifeCycle(context, '/path/to/mock/manifest'); - - expect(withProgressSpy).to.be.calledOnce; - expect(triggerManifestWsStub).to.be.calledOnce; - expect(exhortApiStackAnalysisStub).to.be.calledOnce; - }); - - test('validateSnykToken should execute stackAnalysisServices.getSnykTokenValidationService if a valid token is provided', async () => { - const getApiConfigStub = sandbox.stub(Config, 'getApiConfig').returns({ - exhortSnykToken: 'mockToken' - }); - const getSnykTokenValidationServiceStub = sandbox.stub(stackAnalysisServices, 'getSnykTokenValidationService'); - - await stackanalysismodule.validateSnykToken(); - - expect(getApiConfigStub).to.be.calledOnce; - expect(getSnykTokenValidationServiceStub.calledOnceWithExactly({ EXHORT_SNYK_TOKEN: 'mockToken', 'EXHORT_DEV_MODE': process.env.VSCEXT_EXHORT_DEV_MODE, 'RHDA_TOKEN': process.env.VSCEXT_TELEMETRY_ID, 'RHDA_SOURCE': process.env.VSCEXT_UTM_SOURCE })).to.be.true; - }); - - test('validateSnykToken should show information message if no token is provided', async () => { - const getApiConfigStub = sandbox.stub(Config, 'getApiConfig').returns({ - exhortSnykToken: '' - }); - const showInformationMessageStub = sandbox.stub(vscode.window, 'showInformationMessage'); - - await stackanalysismodule.validateSnykToken(); - - expect(getApiConfigStub).to.be.calledOnce; - expect(showInformationMessageStub).to.be.calledOnce; - }); - -}); diff --git a/test/stackAnalysisService.test.ts b/test/stackAnalysisService.test.ts deleted file mode 100644 index 08e398969..000000000 --- a/test/stackAnalysisService.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import * as chai from 'chai'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; -import * as fs from 'fs'; -import * as vscode from 'vscode'; - -import * as stackAnalysisServices from '../src/stackAnalysisService'; -import exhort from '@RHEcosystemAppEng/exhort-javascript-api'; - - -const expect = chai.expect; -chai.use(sinonChai); - -suite('stacknalysis Services', () => { - let sandbox: sinon.SinonSandbox; - const options = {}; - - setup(() => { - sandbox = sinon.createSandbox(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('exhortApiStackAnalysis should return HTML', async () => { - const pathToManifest = 'sampleMavenApp/pom.xml'; - - const result = await stackAnalysisServices.exhortApiStackAnalysis(pathToManifest, options); - - // Compare the result with the mocked response - const mockHtmlResponse = fs.readFileSync('sampleMavenApp/response.html', 'utf8'); - expect(result).to.equal(mockHtmlResponse); - }); - - test('exhortApiStackAnalysis should return error', async () => { - const pathToManifest = '/path/to/mock/pom.xml'; - sandbox.stub(exhort, 'stackAnalysis').rejects(new Error('Mock error message')); - expect(await stackAnalysisServices.exhortApiStackAnalysis(pathToManifest, options)).to.throw(new Error('Mock error message')); - - }); - - test('getSnykTokenValidationService should show Snyk Token Validated message on 200 status code', async () => { - const showInformationMessage = sandbox.stub(vscode.window, 'showInformationMessage'); - sandbox.stub(exhort, 'validateToken').resolves(200); - - await stackAnalysisServices.getSnykTokenValidationService(options); - - expect(showInformationMessage).to.be.calledWith('Snyk Token Validated Successfully'); - }); - - test('getSnykTokenValidationService should show appropriate warning message on non-200 status code', async () => { - const showWarningMessage = sandbox.stub(vscode.window, 'showWarningMessage'); - - const statusCodes = [400, 401, 403, 429]; - for (const statusCode of statusCodes) { - sandbox.stub(exhort, 'validateToken').resolves(statusCode); - - await stackAnalysisServices.getSnykTokenValidationService(options); - - expect(showWarningMessage).to.be.calledWith(sandbox.match(new RegExp(`^.*Status: ${statusCode}$`))); - } - - // Additional test for an unknown status code - sandbox.stub(exhort, 'validateToken').resolves(500); - - await stackAnalysisServices.getSnykTokenValidationService(options); - - expect(showWarningMessage).to.be.calledWith('Failed to validate token. Status: 500'); - }); - - test('getSnykTokenValidationService should handle error', async () => { - sandbox.stub(exhort, 'validateToken').rejects(new Error('Mock error message')); - - expect(await stackAnalysisServices.getSnykTokenValidationService(options)).to.throw(new Error('Mock error message')); - }); -}); diff --git a/test/tokenValidation.test.ts b/test/tokenValidation.test.ts new file mode 100644 index 000000000..d03be7b4f --- /dev/null +++ b/test/tokenValidation.test.ts @@ -0,0 +1,53 @@ +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as vscode from 'vscode'; + +import { globalConfig } from '../src/config'; +import { validateSnykToken } from '../src/tokenValidation' +import * as exhortServices from '../src/exhortServices' +import { snykURL } from '../src/constants'; + +const expect = chai.expect; +chai.use(sinonChai); + +suite('TokenValidation module', () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('should validate non-empty Snyk token', async () => { + globalConfig.exhortSnykToken = 'mockToken'; + globalConfig.telemetryId = 'mockId'; + const options = { + 'RHDA_TOKEN': 'mockId', + 'RHDA_SOURCE': 'vscode', + 'EXHORT_SNYK_TOKEN': 'mockToken' + }; + + const exhortServicesStub = sandbox.stub(exhortServices, 'tokenValidationService'); + + await validateSnykToken(); + + expect(exhortServicesStub.calledOnceWithExactly(options, 'Snyk')).to.be.true; + }); + + test('should validate empty Snyk token', async () => { + globalConfig.exhortSnykToken = ''; + const expectedMsg = `Please note that if you fail to provide a valid Snyk Token in the extension workspace settings, Snyk vulnerabilities will not be displayed. To resolve this issue, please obtain a valid token from the following link: [here](${snykURL}).`; + + const showInformationMessageStub = sandbox.stub(vscode.window, 'showInformationMessage'); + + await validateSnykToken(); + + const showInformationMessageCall = showInformationMessageStub.getCall(0); + const showInformationMessageMsg = showInformationMessageCall.args[0]; + expect(showInformationMessageMsg.replace(/\s+/g, ' ').replace(/\n/g, ' ')).to.equal(expectedMsg); + }); +}); \ No newline at end of file