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/package-lock.json b/package-lock.json index 4fd48441e..34c9767d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "version": "0.7.4", "license": "Apache-2.0", "dependencies": { - "@fabric8-analytics/fabric8-analytics-lsp-server": "^0.7.1-ea.18", + "@fabric8-analytics/fabric8-analytics-lsp-server": "^0.8.1-ea.2", "@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.2", "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.0", "extraneous": true, "license": "Apache-2.0", "dependencies": { - "@RHEcosystemAppEng/exhort-javascript-api": "^0.0.2-ea.43", + "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.1-ea.0", "@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.8.1-ea.2", + "resolved": "https://npm.pkg.github.com/download/@fabric8-analytics/fabric8-analytics-lsp-server/0.8.1-ea.2/06766b9e951f2ce98d6d920bd8fcb39c9cc353eb", + "integrity": "sha512-+ROzajm05CeICOW6u6s4rkyON74ocXS5MeL+A8/+kG2edGwrKnemckrSpYTNrrrGO/jAZjCB44j1F53UUodm+g==", "license": "Apache-2.0", "dependencies": { - "@RHEcosystemAppEng/exhort-javascript-api": "^0.0.2-ea.49", + "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.1-ea.2", "@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.2", + "resolved": "https://npm.pkg.github.com/download/@RHEcosystemAppEng/exhort-javascript-api/0.1.1-ea.2/f98795b7c1e45eaccf013d4019627ff133d01d19", + "integrity": "sha512-nom7XFXqeNtMcHoUZDJbgFxJOI8ZNjRJZCwCHtBiP2Yt/QtCqf67aes5vfPUX9va/wiaDNcNcLJcvaRcgCGJvA==", "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 f8b268420..7862e8e99 100644 --- a/package.json +++ b/package.json @@ -187,7 +187,19 @@ "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.exhortOSSIndexUser": { + "type": "string", + "default": "", + "description": "Red Hat Dependency Analytics authentication username for OSS Index.", + "scope": "window" + }, + "redHatDependencyAnalytics.exhortOSSIndexToken": { + "type": "string", + "default": "", + "description": "Red Hat Dependency Analytics authentication token for OSS Index.", "scope": "window" }, "redHatDependencyAnalytics.matchManifestVersions": { @@ -284,9 +296,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.8.1-ea.2", "@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.2", "fs": "^0.0.1-security", "path": "^0.12.7", "vscode-languageclient": "^8.1.0" diff --git a/src/caNotification.ts b/src/caNotification.ts index 1a9eacee5..d46c90af0 100644 --- a/src/caNotification.ts +++ b/src/caNotification.ts @@ -1,26 +1,64 @@ 'use strict'; interface CANotificationData { - data: string; + errorMessage: string; done: boolean; uri: string; diagCount: number; - vulnCount: number; + vulnCount: Map; } 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'; 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); + } + + private singularOrPlural(num: number): string { + return num === 1 ? CANotification.VULNERABILITY : CANotification.VULNERABILITIES; + } + + private capitalizeEachWord(inputString: string): string { + return inputString.replace(/\b\w/g, (match) => match.toUpperCase()); + } + + private vulnCountText(): string { + return this.totalVulnCount > 0 ? `${this.totalVulnCount} direct ${this.singularOrPlural(this.totalVulnCount)}` : `no ${CANotification.VULNERABILITIES}`; + } + + private inProgressText(): string { + return `$(${CANotification.SYNC_SPIN}) Dependency analysis in progress`; + } + + private warningText(): string { + return `$(${CANotification.WARNING}) ${this.vulnCountText()} found for all the providers combined`; + } + + private defaultText(): string { + return `$(${CANotification.SHIELD})$(${CANotification.CHECK})`; + } + + public errorMsg(): string { + return this.errorMessage; } public origin(): string { @@ -36,23 +74,20 @@ class CANotification { } 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, ''); } 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/commands.ts b/src/commands.ts index 7f5062c9b..693b9c19b 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -4,13 +4,8 @@ * 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_STACK_LOGS = 'fabric8.fabric8AnalyticsStackLogs'; -export const TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION = 'fabric8.RHRepositoryRecommendationNotification'; +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'; \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index ed067b87d..e1f9a8ee7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,55 +2,97 @@ import * as vscode from 'vscode'; -export function getApiConfig(): any { - return vscode.workspace.getConfiguration('redHatDependencyAnalytics'); -} +import { GlobalState } 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'; -} +class Config { + telemetryId: string; + triggerFullStackAnalysis: string; + utmSource: string; + exhortSnykToken: string; + exhortOSSIndexUser: string; + exhortOSSIndexToken: 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'; -} + /** + * Initializes a new instance of the EnvironmentData class with default extension workspace settings. + */ + constructor() { + this.loadData(); + this.setProcessEnv(); + } -export function getPython3Executable(): string { - const python3Path: string = vscode.workspace - .getConfiguration('python3.executable') - .get('path'); - return python3Path ? python3Path : 'python3'; -} + private getApiConfig(): any { + return vscode.workspace.getConfiguration('redHatDependencyAnalytics'); + } -export function getPip3Executable(): string { - const pip3Path: string = vscode.workspace - .getConfiguration('pip3.executable') - .get('path'); - return pip3Path ? pip3Path : 'pip3'; -} + private getExecutableConfig(exe: string): string { + const exePath: string = vscode.workspace + .getConfiguration(`${exe}.executable`) + .get('path'); + return exePath ? exePath : exe; + } -export function getPythonExecutable(): string { - const pythonPath: string = vscode.workspace - .getConfiguration('python.executable') - .get('path'); - return pythonPath ? pythonPath : 'python'; -} + loadData() { + const apiConfig = this.getApiConfig(); + + this.triggerFullStackAnalysis = commands.TRIGGER_FULL_STACK_ANALYSIS; + this.utmSource = GlobalState.UTM_SOURCE; + this.exhortSnykToken = apiConfig.exhortSnykToken; + this.exhortOSSIndexUser = apiConfig.exhortOSSIndexUser; + this.exhortOSSIndexToken = apiConfig.exhortOSSIndexToken; + this.matchManifestVersions = apiConfig.matchManifestVersions ? 'true' : 'false'; + this.rhdaReportFilePath = apiConfig.redHatDependencyAnalyticsReportFilePath; + this.exhortMvnPath = this.getExecutableConfig(this.DEFAULT_MVN_EXECUTABLE); + this.exhortNpmPath = this.getExecutableConfig(this.DEFAULT_NPM_EXECUTABLE); + this.exhortGoPath = this.getExecutableConfig(this.DEFAULT_GO_EXECUTABLE); + this.exhortPython3Path = this.getExecutableConfig(this.DEFAULT_PYTHON3_EXECUTABLE); + this.exhortPip3Path = this.getExecutableConfig(this.DEFAULT_PIP3_EXECUTABLE); + this.exhortPythonPath = this.getExecutableConfig(this.DEFAULT_PYTHON_EXECUTABLE); + this.exhortPipPath = this.getExecutableConfig(this.DEFAULT_PIP_EXECUTABLE); + } + + private setProcessEnv() { + process.env['VSCEXT_TRIGGER_FULL_STACK_ANALYSIS'] = this.triggerFullStackAnalysis; + process.env['VSCEXT_UTM_SOURCE'] = this.utmSource; + process.env['VSCEXT_EXHORT_SNYK_TOKEN'] = this.exhortSnykToken; + process.env['VSCEXT_EXHORT_OSS_INDEX_USER'] = this.exhortOSSIndexUser; + process.env['VSCEXT_EXHORT_OSS_INDEX_TOKEN'] = this.exhortOSSIndexToken; + 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'; + async authorizeRHDA(context) { + this.telemetryId = await getTelemetryId(context); + process.env['VSCEXT_TELEMETRY_ID'] = this.telemetryId; + } } + +const globalConfig = new Config(); + +export { globalConfig }; + diff --git a/src/constants.ts b/src/constants.ts index 3c3decaa8..2152596ed 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,7 +8,7 @@ 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 { @@ -38,8 +38,10 @@ export const extensionQualifiedId = `redhat.${extensionId}`; export const registrationURL = 'https://app.snyk.io/signup/?utm_medium=Partner&utm_source=RedHat&utm_campaign=Code-Ready-Analytics-2020&utm_content=Register'; // 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'; +// URL to OSS Index webpage +export const ossIndexURL = 'https://ossindex.sonatype.org/'; // 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 similarity index 100% rename from src/DepOutputChannel.ts rename to src/depOutputChannel.ts diff --git a/src/dependencyReportPanel.ts b/src/dependencyReportPanel.ts index abab9c6ed..c8869ad6e 100644 --- a/src/dependencyReportPanel.ts +++ b/src/dependencyReportPanel.ts @@ -1,8 +1,8 @@ import * as vscode from 'vscode'; import * as templates from './template'; -import { Titles, defaultRedhatDependencyAnalyticsReportFilePath } from './constants'; -import * as config from './config'; +import { Titles, defaultRhdaReportFilePath } from './constants'; +import { globalConfig } from './config'; import * as fs from 'fs'; const loaderTmpl = templates.LOADER_TEMPLATE; @@ -132,11 +132,11 @@ export class DependencyReportPanel { } private _disposeReport() { - const apiConfig = config.getApiConfig(); - if (fs.existsSync(apiConfig.redHatDependencyAnalyticsReportFilePath || defaultRedhatDependencyAnalyticsReportFilePath)) { + const reportfilePath = globalConfig.rhdaReportFilePath || defaultRhdaReportFilePath; + 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 71% rename from src/stackAnalysisService.ts rename to src/exhortServices.ts index b96db7078..ad50b6cf1 100644 --- a/src/stackAnalysisService.ts +++ b/src/exhortServices.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import exhort from '@RHEcosystemAppEng/exhort-javascript-api'; -export const exhortApiStackAnalysis = (pathToManifest, options) => { +export const stackAnalysisService = (pathToManifest, options) => { return new Promise(async (resolve, reject) => { try { // Get stack analysis in HTML format @@ -15,7 +15,7 @@ export const exhortApiStackAnalysis = (pathToManifest, options) => { }); }; -export const getSnykTokenValidationService = async (options) => { +export const tokenValidationService = async (options, source) => { try { // Get token validation status code @@ -24,19 +24,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 ) { diff --git a/src/extension.ts b/src/extension.ts index 60e21948b..731a42b4f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,18 +7,18 @@ 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 } 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, validateOSSIndexToken } from './tokenValidation'; let lspClient: LanguageClient; @@ -26,32 +26,26 @@ export let outputChannelDep: DepOutputChannel; 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 rhRepositoryRecommendationNotification = vscode.commands.registerCommand( - commands.TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION, - () => { - const msg = `Important: If you apply Red Hat Dependency Analytics recommendations, - make sure the Red Hat GA Repository (${redhatMavenRepository}) has been added to your project configuration. - This ensures that the applied dependencies work correctly. - Learn how to add the repository: [Click here](${redhatMavenRepositoryDocumentationURL})`; - vscode.window.showWarningMessage(msg); - } - ); - - const disposableStackLogs = vscode.commands.registerCommand( + const disposableStackLogsCommand = vscode.commands.registerCommand( commands.TRIGGER_STACK_LOGS, () => { if (outputChannelDep) { @@ -64,13 +58,10 @@ export function activate(context: vscode.ExtensionContext) { 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') @@ -106,11 +97,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 +108,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 +123,47 @@ 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( - rhRepositoryRecommendationNotification, - disposableFullStack, - disposableStackLogs, + disposableStackAnalysisCommand, + disposableStackLogsCommand, 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(); + } + if (event.affectsConfiguration('redHatDependencyAnalytics.exhortOSSIndexUser') || event.affectsConfiguration('redHatDependencyAnalytics.exhortOSSIndexToken')) { + validateOSSIndexToken(); } - // add more token providers here... }); } -export function initOutputChannel(): DepOutputChannel { - const outputChannelDepInit = new DepOutputChannel(); - return outputChannelDepInit; -} - export function deactivate(): Thenable { if (!lspClient) { return undefined; @@ -190,44 +172,48 @@ export function deactivate(): Thenable { } 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}`)); } } } 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 +228,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..1eccb0f92 100644 --- a/src/redhatTelemetry.ts +++ b/src/redhatTelemetry.ts @@ -4,6 +4,8 @@ import { getRedHatService, TelemetryEvent, TelemetryService } from '@redhat-deve export 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', @@ -35,4 +37,11 @@ export async function record(context: vscode.ExtensionContext, eventName: string export async function startUp(context: vscode.ExtensionContext) { telemetryServiceObj = await telemetryService(context); await telemetryServiceObj?.sendStartupEvent(); +} + +export async function getTelemetryId(context) { + const redhatService = await getRedHatService(context); + const redhatIdProvider = await redhatService.getIdProvider(); + const telemetryId = await redhatIdProvider.getRedHatUUID(); + return telemetryId; } \ No newline at end of file diff --git a/src/stackAnalysis.ts b/src/stackAnalysis.ts new file mode 100644 index 000000000..be4b840c6 --- /dev/null +++ b/src/stackAnalysis.ts @@ -0,0 +1,136 @@ +'use strict'; + +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; + +import { defaultRhdaReportFilePath, StatusMessages, Titles } from './constants'; +import { stackAnalysisService } from './exhortServices'; +import { DependencyReportPanel } from './dependencyReportPanel'; +import { globalConfig } from './config'; + +const supportedFiles = [ + 'pom.xml', + 'package.json', + 'go.mod', + 'requirements.txt' +]; + +function updateWebviewPanel(data) { + if (DependencyReportPanel.currentPanel) { + DependencyReportPanel.currentPanel.doUpdatePanel(data); + } +} + +function writeReportToFile(resp) { + return new Promise((resolve, reject) => { + const reportFilePath = globalConfig.rhdaReportFilePath || defaultRhdaReportFilePath; + const reportDirectoryPath = path.dirname(reportFilePath); + + if (!fs.existsSync(reportDirectoryPath)) { + fs.mkdirSync(reportDirectoryPath, { recursive: true }); + } + + fs.writeFile(reportFilePath, resp, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +async function executeStackAnalysis(manifestFilePath) { + try { + await vscode.window.withProgress({ location: vscode.ProgressLocation.Window, title: Titles.EXT_TITLE }, async p => { + return new Promise(async (resolve, reject) => { + try { + 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; + } + + if (globalConfig.exhortOSSIndexUser !== '' && globalConfig.exhortOSSIndexToken !== '') { + options['EXHORT_OSS_INDEX_USER'] = globalConfig.exhortOSSIndexUser; + options['EXHORT_OSS_INDEX_TOKEN'] = globalConfig.exhortOSSIndexToken; + } + + // execute stack analysis + await stackAnalysisService(manifestFilePath, options) + .then(async (resp) => { + p.report({ + message: StatusMessages.WIN_GENERATING_DEPENDENCIES + }); + + await writeReportToFile(resp); + updateWebviewPanel(resp); + + p.report({ + message: StatusMessages.WIN_SUCCESS_DEPENDENCY_ANALYSIS + }); + + resolve(); + }) + .catch(err => { + p.report({ + message: StatusMessages.WIN_FAILURE_DEPENDENCY_ANALYSIS + }); + + reject(err); + }); + } catch (err) { + p.report({ + message: StatusMessages.WIN_ANALYZING_DEPENDENCIES + }); + + reject(err); + } + }); + }); + } catch (err) { + throw (err); + } +} + +async function triggerWebviewPanel(context) { + await globalConfig.authorizeRHDA(context); + DependencyReportPanel.createOrShowWebviewPanel(); +} + +async function generateRHDAReport(context, uri) { + if (uri.fsPath && supportedFiles.includes(path.basename(uri.fsPath))) { + try { + + await triggerWebviewPanel(context); + await executeStackAnalysis(uri.fsPath); + + } catch (error) { + updateWebviewPanel('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/tokenValidation.ts b/src/tokenValidation.ts new file mode 100644 index 000000000..04a86317e --- /dev/null +++ b/src/tokenValidation.ts @@ -0,0 +1,61 @@ +'use strict'; + +import * as vscode from 'vscode'; + +import { globalConfig } from './config'; +import { snykURL, ossIndexURL } from './constants'; +import { tokenValidationService } from './exhortServices'; + +export const validateSnykToken = async () => { + 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 const validateOSSIndexToken = async () => { + if (globalConfig.exhortOSSIndexUser !== '' && globalConfig.exhortOSSIndexToken !== '') { + + // set up configuration options for the token validation request + const options = { + 'RHDA_TOKEN': globalConfig.telemetryId, + 'RHDA_SOURCE': globalConfig.utmSource, + 'EXHORT_OSS_INDEX_USER': globalConfig.exhortOSSIndexUser, + 'EXHORT_OSS_INDEX_TOKEN': globalConfig.exhortOSSIndexToken + }; + + // execute token validation + tokenValidationService(options, 'OSS Index'); + + } else { + let msg: string = ''; + + if (globalConfig.exhortOSSIndexUser === '') { + msg += 'OSS Index username has not been provided. '; + } + if (globalConfig.exhortOSSIndexToken === '') { + msg = msg ? 'OSS Index username and token have not been provided. ' : 'OSS Index token has not been provided. '; + } + + msg += `Please note that if you fail to provide valid OSS Index credentials in the extension workspace settings, + OSS Index vulnerabilities will not be displayed. + To resolve this issue, please register and obtain valid credentials from the following link: [here](${ossIndexURL}).`; + + vscode.window.showInformationMessage(msg); + } +}; diff --git a/test/caNotification.test.ts b/test/caNotification.test.ts new file mode 100644 index 000000000..79ea41ed9 --- /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) Dependency 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) Dependency 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) Dependency analysis in progress'); + }); +}); diff --git a/test/caStatusBarProvider.test.ts b/test/caStatusBarProvider.test.ts new file mode 100644 index 000000000..e603acdb6 --- /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) Dependency 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..14fd43f4d 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,44 @@ 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 workspace settings', async () => { + + expect(globalConfig.triggerFullStackAnalysis).to.eq(commands.TRIGGER_FULL_STACK_ANALYSIS); + expect(globalConfig.utmSource).to.eq(GlobalState.UTM_SOURCE); + expect(globalConfig.exhortSnykToken).to.eq(''); + expect(globalConfig.exhortOSSIndexUser).to.eq(''); + expect(globalConfig.exhortOSSIndexToken).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_UTM_SOURCE']).to.eq(GlobalState.UTM_SOURCE); + expect(process.env['VSCEXT_EXHORT_SNYK_TOKEN']).to.eq(''); + expect(process.env['VSCEXT_EXHORT_OSS_INDEX_USER']).to.eq(''); + expect(process.env['VSCEXT_EXHORT_OSS_INDEX_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..6e37ef925 100644 --- a/test/dependencyReportPanel.test.ts +++ b/test/dependencyReportPanel.test.ts @@ -4,10 +4,10 @@ import * as sinonChai from 'sinon-chai'; import * as vscode from 'vscode'; import * as fs from 'fs'; -import * as Config from '../src/config'; +import { globalConfig } 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,9 +81,8 @@ suite('DependencyReportPanel Modules', () => { }); test('dispose should dispose of current panel with RHDA report path setting', async () => { - sandbox.stub(Config, 'getApiConfig').returns({ - redHatDependencyAnalyticsReportFilePath: 'mockFilePath', - }); + globalConfig.rhdaReportFilePath = 'mockFilePath' + const existsSyncStub = sandbox.stub(fs, 'existsSync').returns(true); const unlinkSyncStub = sandbox.stub(fs, 'unlinkSync'); @@ -96,9 +95,8 @@ suite('DependencyReportPanel Modules', () => { }); test('dispose should dispose of current panel with default RHDA report path', async () => { - sandbox.stub(Config, 'getApiConfig').returns({ - redHatDependencyAnalyticsReportFilePath: '', - }); + globalConfig.rhdaReportFilePath = '' + const existsSyncStub = sandbox.stub(fs, 'existsSync').returns(true); const unlinkSyncStub = sandbox.stub(fs, 'unlinkSync'); @@ -106,8 +104,8 @@ suite('DependencyReportPanel Modules', () => { 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..73f9a71d5 --- /dev/null +++ b/test/stackAnalysis.test.ts @@ -0,0 +1,125 @@ +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'; + globalConfig.exhortOSSIndexUser = 'mockUser'; + globalConfig.exhortOSSIndexToken = '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 = ''; + globalConfig.exhortOSSIndexUser = ''; + globalConfig.exhortOSSIndexToken = ''; + + 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 and 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(templates.ERROR_TEMPLATE); + }) + }); + + test('should generate RHDA report for supported file and 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(templates.ERROR_TEMPLATE); + }) + }); + +}); 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..c24fbd525 --- /dev/null +++ b/test/tokenValidation.test.ts @@ -0,0 +1,110 @@ +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, validateOSSIndexToken } from '../src/tokenValidation' +import * as exhortServices from '../src/exhortServices' +import { snykURL, ossIndexURL } 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); + }); + + test('should validate non-empty OSS Index token and user', async () => { + globalConfig.exhortOSSIndexUser = 'mockUser'; + globalConfig.exhortOSSIndexToken = 'mockToken'; + globalConfig.telemetryId = 'mockId'; + const options = { + 'RHDA_TOKEN': 'mockId', + 'RHDA_SOURCE': 'vscode', + 'EXHORT_OSS_INDEX_USER': 'mockUser', + 'EXHORT_OSS_INDEX_TOKEN': 'mockToken' + }; + + const exhortServicesStub = sandbox.stub(exhortServices, 'tokenValidationService'); + + await validateOSSIndexToken(); + + expect(exhortServicesStub.calledOnceWithExactly(options, 'OSS Index')).to.be.true; + }); + + test('should validate empty OSS Index token and user', async () => { + globalConfig.exhortOSSIndexUser = ''; + globalConfig.exhortOSSIndexToken = ''; + const expectedMsg = `OSS Index username and token have not been provided. Please note that if you fail to provide valid OSS Index credentials in the extension workspace settings, OSS Index vulnerabilities will not be displayed. To resolve this issue, please register and obtain valid credentials from the following link: [here](${ossIndexURL}).` + const showInformationMessageStub = sandbox.stub(vscode.window, 'showInformationMessage'); + + await validateOSSIndexToken(); + + const showInformationMessageCall = showInformationMessageStub.getCall(0); + const showInformationMessageMsg = showInformationMessageCall.args[0]; + expect(showInformationMessageMsg.replace(/\s+/g, ' ').replace(/\n/g, ' ')).to.equal(expectedMsg); + }); + + test('should validate empty OSS Index token and non-empty user', async () => { + globalConfig.exhortOSSIndexUser = 'mockUser'; + globalConfig.exhortOSSIndexToken = ''; + const expectedMsg = `OSS Index token has not been provided. Please note that if you fail to provide valid OSS Index credentials in the extension workspace settings, OSS Index vulnerabilities will not be displayed. To resolve this issue, please register and obtain valid credentials from the following link: [here](${ossIndexURL}).` + const showInformationMessageStub = sandbox.stub(vscode.window, 'showInformationMessage'); + + await validateOSSIndexToken(); + + const showInformationMessageCall = showInformationMessageStub.getCall(0); + const showInformationMessageMsg = showInformationMessageCall.args[0]; + expect(showInformationMessageMsg.replace(/\s+/g, ' ').replace(/\n/g, ' ')).to.equal(expectedMsg); + }); + + test('should validate non-empty OSS Index token and empty user', async () => { + globalConfig.exhortOSSIndexUser = ''; + globalConfig.exhortOSSIndexToken = 'mockToken'; + const expectedMsg = `OSS Index username has not been provided. Please note that if you fail to provide valid OSS Index credentials in the extension workspace settings, OSS Index vulnerabilities will not be displayed. To resolve this issue, please register and obtain valid credentials from the following link: [here](${ossIndexURL}).` + const showInformationMessageStub = sandbox.stub(vscode.window, 'showInformationMessage'); + + await validateOSSIndexToken(); + + 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