├── src ├── vscode-extension │ ├── cache │ │ ├── cache.ts │ │ └── backgroundRefreshTimer.ts │ ├── commands │ │ ├── api-commands │ │ │ └── refresh.ts │ │ ├── ui-commands │ │ │ ├── scanCommands.ts │ │ │ ├── assessCommand.ts │ │ │ ├── settingsCommand.ts │ │ │ ├── openActivityBar.ts │ │ │ ├── retrieveVulnerabilityCommand.ts │ │ │ ├── messageHandler.ts │ │ │ └── aboutWebviewHandler.ts │ │ └── index.ts │ ├── utils │ │ ├── toggleContrastPanel.ts │ │ ├── pathResolver.ts │ │ ├── encryptDecrypt.ts │ │ ├── commandHandler │ │ │ ├── commandHandlers.ts │ │ │ └── setting.handler.ts │ │ ├── errorHandling.ts │ │ ├── treeItems.ts │ │ ├── persistanceState.ts │ │ ├── localeMemoryCache.ts │ │ ├── statusBarSeverity.ts │ │ ├── listofAllVulnerabilities.ts │ │ └── constants │ │ │ └── commands.ts │ └── logging │ │ ├── cacheLogger.ts │ │ └── logger.ts ├── declaration.d.ts ├── setupTests.ts ├── webview │ ├── utils │ │ ├── authBase64.ts │ │ ├── postMessage.ts │ │ ├── redux │ │ │ ├── slices │ │ │ │ ├── contrastTheme.ts │ │ │ │ ├── localeSlice.ts │ │ │ │ ├── screenSlice.ts │ │ │ │ ├── vulReport.ts │ │ │ │ ├── projectsSlice.ts │ │ │ │ └── ScanFilter.ts │ │ │ └── store.ts │ │ ├── vulMock.ts │ │ └── formattedText.ts │ ├── components │ │ ├── Tab.tsx │ │ ├── TabGroup.tsx │ │ ├── Input.tsx │ │ ├── TextArea.tsx │ │ ├── DatePicker.tsx │ │ ├── Checkbox.tsx │ │ ├── Button.tsx │ │ └── RadioGroup.tsx │ ├── App.tsx │ ├── screens │ │ ├── Scan │ │ │ └── tabs │ │ │ │ ├── CurrentFile │ │ │ │ └── CurrentFileVul.tsx │ │ │ │ └── AllVulnerability │ │ │ │ └── AllVulnerabilityFiles.tsx │ │ └── Assess │ │ │ └── tabs │ │ │ ├── Filters │ │ │ └── AssessFilters.tsx │ │ │ ├── CurrentFile │ │ │ └── AssessCurrentFile.tsx │ │ │ ├── VulnerabilityReport │ │ │ ├── tabs │ │ │ │ ├── HowToFix.tsx │ │ │ │ ├── HttpRequest.tsx │ │ │ │ └── Overview.tsx │ │ │ └── VulnerabilityReport.tsx │ │ │ └── LibraryReport │ │ │ └── tabs │ │ │ ├── LibraryHowToFix.tsx │ │ │ └── LibraryPath.tsx │ └── hooks │ │ └── useDateTime.ts ├── test │ ├── mocks │ │ ├── vscode.ts │ │ └── testMock.ts │ ├── ui │ │ └── suite │ │ │ └── index.ts │ └── unitTest │ │ ├── webview │ │ ├── CurrentFile.test.tsx │ │ ├── Events.test.tsx │ │ ├── projectSlice.test.tsx │ │ ├── Overview.test.tsx │ │ ├── HowToFix.test.tsx │ │ ├── ScanFilter.test.tsx │ │ ├── Organizationtable.test.tsx │ │ └── messageHandler.test.tsx │ │ └── vscode-extension │ │ ├── encryptDecrypt.test.ts │ │ ├── cacheLogger.test.ts │ │ ├── getScanResultProjectFailure.test.ts │ │ ├── ContrastTreeItem.test.ts │ │ ├── getPackageInformation.test.ts │ │ ├── getOrganisationName.test.ts │ │ ├── getScanResults.test.ts │ │ └── getVulnerabilityByTraceId.test.ts ├── extension.ts ├── styles │ ├── about.css │ └── screens │ │ └── scan.scss └── l10n.ts ├── .env ├── .idea ├── .gitignore ├── vcs.xml ├── git_toolbox_blame.xml ├── misc.xml ├── modules.xml └── contrast-vscode-plugin.iml ├── assets ├── settings.png ├── contrast-scan.png ├── contrastIcon.png ├── information.png ├── contrast-assess.png ├── outdatedLibrary.png ├── CS_logo_white_bg.jpg ├── restrictedLicenses.png ├── restrictedLibraries.png ├── readme-images │ ├── about-page.png │ ├── scan-filter.png │ ├── assess-filter.png │ ├── assess-filter1.png │ ├── assess-current-file.png │ ├── gitpod │ │ ├── about-page.png │ │ ├── scan-filter.png │ │ ├── assess-filter.png │ │ ├── assess-filter1.png │ │ ├── scan-current-file.png │ │ ├── assess-current-file.png │ │ ├── assess-library-report.png │ │ ├── configuration-settings.png │ │ ├── assess-vulnerability-report.png │ │ └── scan-vulnerability-report.png │ ├── scan-current-file.png │ ├── vscode │ │ ├── about-page.png │ │ ├── scan-filter.png │ │ ├── assess-filter.png │ │ ├── assess-filter1.png │ │ ├── scan-current-file.png │ │ ├── assess-current-file.png │ │ ├── assess-library-report.png │ │ ├── configuration-settings.png │ │ ├── assess-vulnerability-report.png │ │ └── scan-vulnerability-report.png │ ├── assess-library-report.png │ ├── configuration-settings.png │ ├── scan-vulnerability-report.png │ └── assess-vulnerability-report.png ├── download.svg └── CS_logo_RGB_IDE.svg ├── .gitignore ├── .babelrc ├── .vscode-test.mjs ├── __mocks__ └── vscode.js ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── tasks.json └── settings.json ├── LICENSE ├── tsconfig.json ├── .vscodeignore ├── scripts └── run-electron-tests.js ├── sonar-project.properties ├── jest.config.js ├── webpack.webview.config.js ├── webpack.config.js ├── eslint.config.mjs ├── vsc-extension-quickstart.md └── CHANGELOG.md /src/vscode-extension/cache/cache.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | ENCRYPTION_SECRET=123912863265612376 2 | 3 | -------------------------------------------------------------------------------- /src/vscode-extension/commands/api-commands/refresh.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; // Add Jest DOM matchers 2 | -------------------------------------------------------------------------------- /assets/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/settings.png -------------------------------------------------------------------------------- /assets/contrast-scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/contrast-scan.png -------------------------------------------------------------------------------- /assets/contrastIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/contrastIcon.png -------------------------------------------------------------------------------- /assets/information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/information.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | logs 7 | out 8 | coverage/* 9 | .vscode/logs 10 | -------------------------------------------------------------------------------- /assets/contrast-assess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/contrast-assess.png -------------------------------------------------------------------------------- /assets/outdatedLibrary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/outdatedLibrary.png -------------------------------------------------------------------------------- /assets/CS_logo_white_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/CS_logo_white_bg.jpg -------------------------------------------------------------------------------- /assets/restrictedLicenses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/restrictedLicenses.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /assets/restrictedLibraries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/restrictedLibraries.png -------------------------------------------------------------------------------- /assets/readme-images/about-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/about-page.png -------------------------------------------------------------------------------- /assets/readme-images/scan-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/scan-filter.png -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: 'out/test/**/*.test.js', 5 | }); 6 | -------------------------------------------------------------------------------- /assets/readme-images/assess-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/assess-filter.png -------------------------------------------------------------------------------- /assets/readme-images/assess-filter1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/assess-filter1.png -------------------------------------------------------------------------------- /assets/readme-images/assess-current-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/assess-current-file.png -------------------------------------------------------------------------------- /assets/readme-images/gitpod/about-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/gitpod/about-page.png -------------------------------------------------------------------------------- /assets/readme-images/gitpod/scan-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/gitpod/scan-filter.png -------------------------------------------------------------------------------- /assets/readme-images/scan-current-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/scan-current-file.png -------------------------------------------------------------------------------- /assets/readme-images/vscode/about-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/vscode/about-page.png -------------------------------------------------------------------------------- /assets/readme-images/vscode/scan-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/vscode/scan-filter.png -------------------------------------------------------------------------------- /assets/readme-images/assess-library-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/assess-library-report.png -------------------------------------------------------------------------------- /assets/readme-images/gitpod/assess-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/gitpod/assess-filter.png -------------------------------------------------------------------------------- /assets/readme-images/gitpod/assess-filter1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/gitpod/assess-filter1.png -------------------------------------------------------------------------------- /assets/readme-images/vscode/assess-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/vscode/assess-filter.png -------------------------------------------------------------------------------- /assets/readme-images/vscode/assess-filter1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/vscode/assess-filter1.png -------------------------------------------------------------------------------- /src/webview/utils/authBase64.ts: -------------------------------------------------------------------------------- 1 | export const authBase64 = (user_name: string, service_key: string): string => { 2 | return btoa(`${user_name}:${service_key}`); 3 | }; 4 | -------------------------------------------------------------------------------- /assets/readme-images/configuration-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/configuration-settings.png -------------------------------------------------------------------------------- /assets/readme-images/gitpod/scan-current-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/gitpod/scan-current-file.png -------------------------------------------------------------------------------- /assets/readme-images/vscode/scan-current-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/vscode/scan-current-file.png -------------------------------------------------------------------------------- /assets/readme-images/gitpod/assess-current-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/gitpod/assess-current-file.png -------------------------------------------------------------------------------- /assets/readme-images/scan-vulnerability-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/scan-vulnerability-report.png -------------------------------------------------------------------------------- /assets/readme-images/vscode/assess-current-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/vscode/assess-current-file.png -------------------------------------------------------------------------------- /assets/readme-images/assess-vulnerability-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/assess-vulnerability-report.png -------------------------------------------------------------------------------- /assets/readme-images/gitpod/assess-library-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/gitpod/assess-library-report.png -------------------------------------------------------------------------------- /assets/readme-images/gitpod/configuration-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/gitpod/configuration-settings.png -------------------------------------------------------------------------------- /assets/readme-images/vscode/assess-library-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/vscode/assess-library-report.png -------------------------------------------------------------------------------- /assets/readme-images/vscode/configuration-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/vscode/configuration-settings.png -------------------------------------------------------------------------------- /assets/readme-images/gitpod/assess-vulnerability-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/gitpod/assess-vulnerability-report.png -------------------------------------------------------------------------------- /assets/readme-images/gitpod/scan-vulnerability-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/gitpod/scan-vulnerability-report.png -------------------------------------------------------------------------------- /assets/readme-images/vscode/assess-vulnerability-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/vscode/assess-vulnerability-report.png -------------------------------------------------------------------------------- /assets/readme-images/vscode/scan-vulnerability-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-vscode-plugin/main/assets/readme-images/vscode/scan-vulnerability-report.png -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/git_toolbox_blame.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /__mocks__/vscode.js: -------------------------------------------------------------------------------- 1 | // __mocks__/vscode.js 2 | module.exports = { 3 | env: { 4 | language: 'en', 5 | appName: 'VSCode', 6 | }, 7 | workspace: { 8 | workspaceFolders: [{ uri: { fsPath: '/path/to/mock/workspace' } }], 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "jsxSingleQuote": false, 8 | "bracketSpacing": true, 9 | "arrowParens": "always", 10 | "printWidth": 80 11 | } 12 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "amodio.tsl-problem-matcher", 7 | "ms-vscode.extension-test-runner" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/test/mocks/vscode.ts: -------------------------------------------------------------------------------- 1 | // __mocks__/vscode.d.ts 2 | declare module 'vscode' { 3 | export class Memento { 4 | /* eslint-disable @typescript-eslint/no-explicit-any */ 5 | private readonly store: Record; 6 | 7 | constructor(); 8 | get(key: string): any; 9 | update(key: string, value: any): Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/webview/components/Tab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TabProps } from '../../common/types'; 3 | export function Tab({ title, isActive, onClick }: TabProps) { 4 | return ( 5 |
6 |
{title}
7 | {isActive === true ?
: null} 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /.idea/contrast-vscode-plugin.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/vscode-extension/utils/toggleContrastPanel.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | let isOpened: boolean = false; 4 | const toggleContrastPanel = (): void => { 5 | if (!isOpened) { 6 | vscode.commands.executeCommand('workbench.action.togglePanel'); 7 | } 8 | vscode.commands.executeCommand('workbench.view.extension.ContrastPanel'); 9 | isOpened = true; 10 | }; 11 | 12 | export { toggleContrastPanel }; 13 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { disposeCommads, registerCommands } from './vscode-extension/commands'; 3 | import { disposeCache } from './vscode-extension/cache/cacheManager'; 4 | 5 | export async function activate(context: vscode.ExtensionContext) { 6 | await registerCommands(context); 7 | } 8 | 9 | export async function deactivate() { 10 | disposeCommads(); 11 | await disposeCache(); 12 | } 13 | -------------------------------------------------------------------------------- /src/webview/utils/postMessage.ts: -------------------------------------------------------------------------------- 1 | import { CommandRequest } from '../../common/types'; 2 | 3 | declare const acquireVsCodeApi: () => { 4 | postMessage: (request: CommandRequest) => void; 5 | }; 6 | 7 | const vscodeApi = acquireVsCodeApi(); 8 | 9 | const webviewPostMessage = (request: CommandRequest) => { 10 | if (vscodeApi !== null && vscodeApi !== undefined) { 11 | vscodeApi.postMessage(request); 12 | } 13 | }; 14 | 15 | export { webviewPostMessage }; 16 | -------------------------------------------------------------------------------- /assets/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/vscode-extension/utils/pathResolver.ts: -------------------------------------------------------------------------------- 1 | import { Uri, Webview } from 'vscode'; 2 | import { globalExtentionUri } from '../commands'; 3 | 4 | class PathResolver { 5 | rootPath!: Webview; 6 | 7 | constructor(resource: Webview) { 8 | this.rootPath = resource; 9 | } 10 | 11 | public resolve(segments: string[]) { 12 | return this.rootPath.asWebviewUri( 13 | Uri.joinPath(globalExtentionUri.extensionUri, ...segments) 14 | ); 15 | } 16 | } 17 | 18 | export { PathResolver }; 19 | -------------------------------------------------------------------------------- /src/styles/about.css: -------------------------------------------------------------------------------- 1 | * { 2 | user-select: none; 3 | } 4 | body { 5 | padding: 10px; 6 | } 7 | 8 | table, 9 | th, 10 | td { 11 | border: 1px solid #313131; 12 | border-collapse: collapse; 13 | } 14 | 15 | table td { 16 | min-width: 200px; 17 | padding: 8px; 18 | } 19 | 20 | .indent { 21 | padding-left: 20px; 22 | } 23 | 24 | hr { 25 | background-color: rgb(20, 19, 19); 26 | border: none; 27 | height: 1px; 28 | margin: 20px 0px; 29 | } 30 | 31 | .about-ul { 32 | li { 33 | line-height: 1.3; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025, Contrast Security, OSS. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /src/webview/components/TabGroup.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { TabGroupProps } from '../../common/types'; 3 | 4 | export function TabGroup({ onTabChange, children }: TabGroupProps) { 5 | const handleClick = (id: number) => () => { 6 | onTabChange(id); 7 | }; 8 | 9 | return ( 10 |
11 | {React.Children.map(children, (child, index) => 12 | React.cloneElement(child as ReactElement, { 13 | onClick: handleClick(index + 1), 14 | }) 15 | )} 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/webview/utils/redux/slices/contrastTheme.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const contrastTheme: Record = { 4 | data: null, 5 | }; 6 | 7 | const ThemeSlicer = createSlice({ 8 | name: 'contrastTheme', 9 | initialState: contrastTheme, 10 | reducers: { 11 | setContrastTheme: (state, action) => { 12 | state.data = action.payload; 13 | }, 14 | }, 15 | }); 16 | 17 | export const { setContrastTheme } = ThemeSlicer.actions; 18 | const ThemeReducer = ThemeSlicer.reducer; 19 | 20 | export { ThemeReducer }; 21 | -------------------------------------------------------------------------------- /src/webview/utils/redux/slices/localeSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { LocaleState } from '../../../../common/types'; 3 | 4 | const localeState: LocaleState = { 5 | data: null, 6 | }; 7 | 8 | const LocaleSlicer = createSlice({ 9 | name: 'locale', 10 | initialState: localeState, 11 | reducers: { 12 | setLocale: (state, action) => { 13 | state.data = action.payload; 14 | }, 15 | }, 16 | }); 17 | 18 | export const { setLocale } = LocaleSlicer.actions; 19 | const LocaleReducer = LocaleSlicer.reducer; 20 | 21 | export { LocaleReducer }; 22 | -------------------------------------------------------------------------------- /src/webview/utils/redux/slices/screenSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { ScreenState } from '../../../../common/types'; 3 | 4 | const screenState: ScreenState = { 5 | data: null, 6 | }; 7 | 8 | const LocaleSlicer = createSlice({ 9 | name: 'screen', 10 | initialState: screenState, 11 | reducers: { 12 | setScreen: (state, action) => { 13 | state.data = action.payload; 14 | }, 15 | }, 16 | }); 17 | 18 | export const { setScreen } = LocaleSlicer.actions; 19 | const ScreenReducer = LocaleSlicer.reducer; 20 | 21 | export { ScreenReducer }; 22 | -------------------------------------------------------------------------------- /src/webview/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { InputProps } from '../../common/types'; 3 | 4 | const Input: FC = ({ 5 | type, 6 | placeholder = '', 7 | onChange, 8 | className = '', 9 | name, 10 | value, 11 | id, 12 | }) => { 13 | return ( 14 | <> 15 | 24 | 25 | ); 26 | }; 27 | 28 | export default Input; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "module": "CommonJS", 6 | "moduleResolution": "node", 7 | "target": "ESNext", 8 | "lib": ["ES2022", "DOM"], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitAny": true, 14 | "allowSyntheticDefaultImports": true, 15 | "strict": true, 16 | "resolveJsonModule": true 17 | }, 18 | "include": ["src/**/*", "src/**/*.d.ts", "jest.setup.ts"], 19 | "exclude": ["node_modules", ".vscode-test", "out", "dist", "test"] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 13 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 14 | "preLaunchTask": "${defaultBuildTask}" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | # Node Modules 2 | node_modules/** 3 | 4 | # Version Control 5 | .git/ 6 | .github/ 7 | .gitignore 8 | .vscode/ 9 | .vscode/** 10 | .env 11 | 12 | # Tests 13 | .vscode-test/** 14 | coverage/ 15 | 16 | # Additional Ignored Files 17 | .gitignore 18 | .yarnrc 19 | vsc-extension-quickstart.md 20 | 21 | # System Files 22 | .DS_Store 23 | Thumbs.db 24 | **/src/webview 25 | 26 | # Bundler Configuration 27 | webpack.config.js 28 | webpack.*.config.js 29 | prettier.config.js 30 | eslint.config.js 31 | tsconfig.json 32 | **/tsconfig.json 33 | **/eslint.config.mjs 34 | 35 | # Source Maps and TypeScript Files 36 | **/*.map 37 | **/*.ts 38 | 39 | # VS Code Test Files 40 | **/.vscode-test.* 41 | -------------------------------------------------------------------------------- /src/vscode-extension/commands/ui-commands/scanCommands.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ContrastPanelInstance } from './webviewHandler'; 3 | import { toggleContrastPanel } from '../../utils/toggleContrastPanel'; 4 | import { 5 | CONSTRAST_SCAN, 6 | EXTENTION_COMMANDS, 7 | } from '../../utils/constants/commands'; 8 | import { aboutWebviewPanelInstance } from './aboutWebviewHandler'; 9 | 10 | const registerScanCommand = vscode.commands.registerCommand( 11 | CONSTRAST_SCAN, 12 | () => { 13 | toggleContrastPanel(); 14 | ContrastPanelInstance.onChangeScreen(EXTENTION_COMMANDS.SCAN_SCREEN); 15 | aboutWebviewPanelInstance.dispose(); 16 | } 17 | ); 18 | 19 | export { registerScanCommand }; 20 | -------------------------------------------------------------------------------- /src/vscode-extension/commands/ui-commands/assessCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ContrastPanelInstance } from './webviewHandler'; 3 | import { toggleContrastPanel } from '../../utils/toggleContrastPanel'; 4 | import { 5 | CONSTRAST_ASSESS, 6 | EXTENTION_COMMANDS, 7 | } from '../../utils/constants/commands'; 8 | import { aboutWebviewPanelInstance } from './aboutWebviewHandler'; 9 | 10 | const registerAssessCommand = vscode.commands.registerCommand( 11 | CONSTRAST_ASSESS, 12 | () => { 13 | toggleContrastPanel(); 14 | ContrastPanelInstance.onChangeScreen(EXTENTION_COMMANDS.ASSESS_SCREEN); 15 | aboutWebviewPanelInstance.dispose(); 16 | } 17 | ); 18 | 19 | export { registerAssessCommand }; 20 | -------------------------------------------------------------------------------- /src/webview/components/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type TextAreaProps = { 4 | placeHolder?: string; 5 | isDisabled?: boolean; 6 | cols?: number; 7 | rows?: number; 8 | onInput?: (e: string) => void; 9 | value: string; 10 | }; 11 | export function TextArea({ 12 | isDisabled = false, 13 | placeHolder = '', 14 | cols = 35, 15 | rows = 7, 16 | onInput, 17 | value, 18 | }: TextAreaProps) { 19 | return ( 20 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/vscode-extension/commands/ui-commands/settingsCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ContrastPanelInstance } from './webviewHandler'; 3 | import { toggleContrastPanel } from '../../utils/toggleContrastPanel'; 4 | import { 5 | CONSTRAST_SETTING, 6 | EXTENTION_COMMANDS, 7 | } from '../../utils/constants/commands'; 8 | import { aboutWebviewPanelInstance } from './aboutWebviewHandler'; 9 | 10 | const registerSettingsCommand = vscode.commands.registerCommand( 11 | CONSTRAST_SETTING, 12 | () => { 13 | toggleContrastPanel(); 14 | ContrastPanelInstance.onChangeScreen(EXTENTION_COMMANDS.SETTING_SCREEN); 15 | aboutWebviewPanelInstance.dispose(); 16 | } 17 | ); 18 | 19 | export { registerSettingsCommand }; 20 | -------------------------------------------------------------------------------- /scripts/run-electron-tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require('path'); 3 | const { runTests } = require('@vscode/test-electron'); 4 | 5 | (async () => { 6 | try { 7 | const extensionDevelopmentPath = path.resolve(__dirname, '..'); // repo root 8 | const extensionTestsPath = path.resolve( 9 | __dirname, 10 | '../out/test/ui/suite/index.js' 11 | ); 12 | 13 | await runTests({ 14 | version: 'stable', 15 | extensionDevelopmentPath, 16 | extensionTestsPath, 17 | launchArgs: ['--disable-extensions'], 18 | }); 19 | } catch (err) { 20 | console.error('Failed to run VS Code UI smoke tests:', err); 21 | process.exit(1); 22 | } 23 | })(); 24 | -------------------------------------------------------------------------------- /src/vscode-extension/utils/encryptDecrypt.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export class SecretService { 4 | //Intialize the secretStorage 5 | private secretStorage: vscode.SecretStorage; 6 | 7 | constructor(secretStorage: vscode.SecretStorage) { 8 | this.secretStorage = secretStorage; 9 | } 10 | 11 | // Store a secret 12 | async storeSecret(key: string, value: string): Promise { 13 | await this.secretStorage.store(key, value); 14 | } 15 | 16 | // Get a secret 17 | async getSecret(key: string): Promise { 18 | return await this.secretStorage.get(key); 19 | } 20 | 21 | // Delete a secret 22 | async deleteSecret(key: string): Promise { 23 | await this.secretStorage.delete(key); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/vscode-extension/utils/commandHandler/commandHandlers.ts: -------------------------------------------------------------------------------- 1 | import { CommandRequest } from '../../../common/types'; 2 | import { WEBVIEW_SCREENS } from '../constants/commands'; 3 | import { AssessCommandHandler } from './assess.handler'; 4 | import { ScanCommandHandler } from './scan.handler'; 5 | import { SettingCommandHandler } from './setting.handler'; 6 | 7 | export const commandHandler = async (data: CommandRequest) => { 8 | const { screen } = data; 9 | switch (screen) { 10 | case WEBVIEW_SCREENS.SETTING: { 11 | return SettingCommandHandler(data); 12 | } 13 | case WEBVIEW_SCREENS.SCAN: { 14 | return ScanCommandHandler(data); 15 | } 16 | case WEBVIEW_SCREENS.ASSESS: { 17 | return AssessCommandHandler(data); 18 | } 19 | } 20 | return null; 21 | }; 22 | -------------------------------------------------------------------------------- /src/webview/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Setting from './screens/Setting/Setting'; 3 | import { useSelector } from 'react-redux'; 4 | import { ContrastScan } from './screens/Scan/Scan'; 5 | import { ReducerTypes } from '../common/types'; 6 | import { ContrastAssess } from './screens/Assess/Assess'; 7 | 8 | function App() { 9 | const screenView = useSelector((state: ReducerTypes) => state.screen.data); 10 | const contrastTheme = useSelector((state: ReducerTypes) => state.theme.data); 11 | 12 | return ( 13 |
14 | {screenView === 1 ? : null} 15 | {screenView === 2 ? : null} 16 | {screenView === 3 ? : null} 17 |
18 | ); 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/webview/utils/redux/slices/vulReport.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { VulReport } from '../../../../common/types'; 3 | 4 | const vulReportState: VulReport = { 5 | currentFile: null, 6 | allFiles: null, 7 | }; 8 | 9 | const vulnerablitySlicer = createSlice({ 10 | name: 'vulReport', 11 | initialState: vulReportState, 12 | reducers: { 13 | getCurrentFileVulnerability: (state, action) => { 14 | state.currentFile = action.payload; 15 | }, 16 | getAllFilesVulnerability: (state, action) => { 17 | state.allFiles = action.payload; 18 | }, 19 | }, 20 | }); 21 | 22 | export const { getCurrentFileVulnerability, getAllFilesVulnerability } = 23 | vulnerablitySlicer.actions; 24 | const vulnerabilityReducer = vulnerablitySlicer.reducer; 25 | 26 | export { vulnerabilityReducer }; 27 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # Encoding of the source code. Default is default system encoding 2 | sonar.sourceEncoding=UTF-8 3 | sonar.language=js 4 | sonar.exclusions=src/webview/**, __mocks__/**, test/**/*, node_modules/**/*, src/**/*.test.tsx, src/**/*.test.ts, src/index.*, src/wrapper/**, utils/**, webpack.*.config.ts, src/mfeUrls.js, src/extension.ts, src/vscode-extension/commands/*,src/vscode-extension/utils/*, src/vscode-extension/logging/*, jest.config.js, webpack.webview.config.js, src/vscode-extension/utils/commandHandler/*, src/vscode-extension/commands/ui-commands/*, webpack.config.js, src/l10n.ts 5 | 6 | # Sonar should be able to grab these from the build context, but for some reason can't 7 | sonar.projectKey=Contrast-Security-Inc_contrast-vscode-plugin 8 | sonar.organization=contrast-security-inc 9 | 10 | sonar.javascript.lcov.reportPaths=coverage/**/lcov.info 11 | -------------------------------------------------------------------------------- /src/vscode-extension/utils/errorHandling.ts: -------------------------------------------------------------------------------- 1 | import { ApiResponse, ResponseData } from '../../common/types'; 2 | 3 | // Helper to resolve success responses 4 | function resolveSuccess( 5 | messageText: string | undefined, 6 | code: number, 7 | 8 | data: ResponseData 9 | ): ApiResponse { 10 | const message = 11 | messageText !== null && messageText !== undefined ? messageText : ''; 12 | return { 13 | status: 'success', 14 | message, 15 | code, 16 | responseData: data, 17 | }; 18 | } 19 | 20 | // Helper to resolve failure responses 21 | function resolveFailure( 22 | messageText: string | undefined, 23 | code: number 24 | ): ApiResponse { 25 | const message = 26 | messageText !== null && messageText !== undefined ? messageText : ''; 27 | return { 28 | status: 'failure', 29 | message, 30 | code, 31 | responseData: null, 32 | }; 33 | } 34 | 35 | export { resolveFailure, resolveSuccess }; 36 | -------------------------------------------------------------------------------- /src/test/ui/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import Mocha from 'mocha'; 3 | import { glob } from 'glob'; 4 | 5 | export async function run(): Promise { 6 | const mocha = new Mocha({ ui: 'tdd', color: true, timeout: 60000 }); 7 | 8 | // Points to out/ui 9 | const testsRoot = path.resolve(__dirname, '..'); 10 | 11 | // Find all test files inside out/ui (including subfolders) 12 | const files = await glob('**/*.test.js', { cwd: testsRoot }); 13 | 14 | // Add each test file to Mocha 15 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 16 | 17 | // Run tests 18 | await new Promise((resolve, reject) => { 19 | try { 20 | mocha.run((failures) => { 21 | if (failures) { 22 | reject(new Error(`${failures} tests failed`)); 23 | } else { 24 | resolve(); 25 | } 26 | }); 27 | } catch (e) { 28 | reject(e); 29 | } 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/webview/utils/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { LocaleReducer } from './slices/localeSlice'; 3 | import { ScreenReducer } from './slices/screenSlice'; 4 | import { ProjectReducer } from './slices/projectsSlice'; 5 | import { vulnerabilityReducer } from './slices/vulReport'; 6 | import { ScanReducer } from './slices/ScanFilter'; 7 | import { ThemeReducer } from './slices/contrastTheme'; 8 | import { AssessFilterReducer } from './slices/assessFilter'; 9 | export const ContrastStore = configureStore({ 10 | reducer: { 11 | i10ln: LocaleReducer, 12 | screen: ScreenReducer, 13 | project: ProjectReducer, 14 | vulnerability: vulnerabilityReducer, 15 | scan: ScanReducer, 16 | theme: ThemeReducer, 17 | assessFilter: AssessFilterReducer, 18 | }, 19 | }); 20 | export default ContrastStore; 21 | export type RootState = ReturnType; 22 | export type AppDispatch = typeof ContrastStore.dispatch; 23 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$ts-webpack-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": ["npm: watch", "npm: watch-tests"], 34 | "problemMatcher": [] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/styles/screens/scan.scss: -------------------------------------------------------------------------------- 1 | /* Scan Features - Layout & Styling */ 2 | 3 | .scan-features { 4 | display: flex; 5 | flex-direction: column; 6 | gap: 25px; 7 | margin-top: 10px; 8 | 9 | .feature { 10 | display: flex; 11 | flex-direction: row; 12 | 13 | .label { 14 | width: 130px; 15 | } 16 | 17 | .feature-fields { 18 | display: grid; 19 | grid-template-columns: 1fr 1fr 1fr; 20 | flex-wrap: wrap; 21 | gap: 10px; 22 | } 23 | .project-dropdown { 24 | width: 200px; 25 | } 26 | 27 | .filter-feature { 28 | display: flex; 29 | flex-direction: row; 30 | gap: 50px; 31 | 32 | .date-container { 33 | display: flex; 34 | flex-direction: column; 35 | gap: 10px; 36 | 37 | .date-inner-container { 38 | display: flex; 39 | flex-direction: row; 40 | gap: 10px; 41 | align-items: center; 42 | 43 | .date-datetime { 44 | display: flex; 45 | flex-direction: row; 46 | gap: 10px; 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off", 13 | "editor.codeActionsOnSave": { 14 | "source.fixAll.eslint": "explicit" 15 | }, 16 | "[json]": { 17 | "editor.defaultFormatter": "vscode.json-language-features" 18 | }, 19 | "editor.formatOnSave": true, 20 | "editor.defaultFormatter": "esbenp.prettier-vscode", 21 | "[javascript]": { 22 | "editor.defaultFormatter": "esbenp.prettier-vscode" 23 | }, 24 | "[typescript]": { 25 | "editor.defaultFormatter": "esbenp.prettier-vscode" 26 | }, 27 | "[typescriptreact]": { 28 | "editor.defaultFormatter": "esbenp.prettier-vscode" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/vscode-extension/logging/cacheLogger.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | const today = new Date(); 5 | // Define the log file path in the application folder 6 | const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; 7 | if (workspaceFolder === undefined) { 8 | vscode.window.showErrorMessage('No workspace folder found.'); 9 | throw new Error('Workspace folder is not available'); 10 | } 11 | 12 | const logFilePath = path.join( 13 | workspaceFolder, 14 | 'application', 15 | `log_${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}.txt` 16 | ); 17 | 18 | // Function to log start and end times and number of records 19 | export function logInfo(startTime: Date, endTime: Date, message: string) { 20 | const logData = ` 21 | Start Time: ${startTime.toISOString()} 22 | End Time: ${endTime.toISOString()} 23 | Message: ${message} 24 | `; 25 | // Ensure the application folder exists 26 | const appFolderPath = path.dirname(logFilePath); 27 | if (!fs.existsSync(appFolderPath)) { 28 | fs.mkdirSync(appFolderPath, { recursive: true }); 29 | } 30 | // Write the log data to the file 31 | fs.appendFileSync(logFilePath, logData + '\n', 'utf8'); 32 | } 33 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // jest.config.js 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'jest-environment-jsdom', 5 | 6 | // 👇 Limit Jest to unit tests only 7 | roots: ['/src/test/unitTest'], 8 | 9 | // Only pick test files inside src/test 10 | testMatch: ['**/?(*.)+(test|spec).+(ts|tsx|js|jsx)'], 11 | 12 | // Ignore compiled output and VS Code test runtime folders 13 | testPathIgnorePatterns: [ 14 | '/node_modules/', 15 | '/dist/', 16 | '/out/', 17 | '/.vscode-test/', 18 | ], 19 | modulePathIgnorePatterns: ['/dist/', '/out/'], 20 | 21 | moduleNameMapper: { 22 | '\\.(css|less|scss|sass)$': 'identity-obj-proxy', 23 | '^vscode$': '/__mocks__/vscode.js', 24 | }, 25 | 26 | setupFilesAfterEnv: ['/src/setupTests.ts'], 27 | 28 | transform: { 29 | '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }], 30 | }, 31 | collectCoverageFrom: [ 32 | '/src/**/*.ts', 33 | '!/src/**/*.d.ts', 34 | '!/src/vscode-extension/commands/**', 35 | '!/src/vscode-extension/logging/**', 36 | '!/src/vscode-extension/utils/**', 37 | '!/src/vscode-extension/utils/commandHandler/**', 38 | '!/src/webview/**', 39 | '!/src/l10n.ts' 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /src/vscode-extension/commands/ui-commands/openActivityBar.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | ContrastNavigationItems, 4 | ContrastTreeItem, 5 | } from '../../utils/treeItems'; 6 | import { CONSTRAST_ACTIVITYBAR } from '../../utils/constants/commands'; 7 | import { aboutWebviewPanelInstance } from './aboutWebviewHandler'; 8 | import { aboutSlotInstance } from '../../utils/helper'; 9 | 10 | class ContrastActivityBar implements vscode.TreeDataProvider { 11 | data: ContrastTreeItem[] = ContrastNavigationItems; 12 | 13 | constructor() {} 14 | 15 | getChildren(): vscode.ProviderResult { 16 | return this.data; 17 | } 18 | 19 | getTreeItem(element: ContrastTreeItem): ContrastTreeItem { 20 | return element; 21 | } 22 | } 23 | 24 | const contrastActivityBarProvider = new ContrastActivityBar(); 25 | const registerContrastActivityBar = vscode.window.createTreeView( 26 | CONSTRAST_ACTIVITYBAR, 27 | { treeDataProvider: contrastActivityBarProvider } 28 | ); 29 | 30 | registerContrastActivityBar.onDidChangeVisibility(async (event) => { 31 | if (event.visible) { 32 | if (aboutSlotInstance.getSlot() === true) { 33 | await aboutWebviewPanelInstance.init(); 34 | aboutSlotInstance.setSlot(false); 35 | } 36 | } else { 37 | aboutWebviewPanelInstance.dispose(); 38 | } 39 | }); 40 | 41 | export { registerContrastActivityBar, ContrastTreeItem }; 42 | -------------------------------------------------------------------------------- /assets/CS_logo_RGB_IDE.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/webview/components/DatePicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useRef, useState } from 'react'; 2 | 3 | interface DatePickerProps { 4 | id: string; 5 | value?: string | null; 6 | onDateChange?: (date: string) => void; 7 | disabled?: boolean; 8 | min?: string | null; 9 | max?: string | null; 10 | } 11 | 12 | const DatePicker: FC = ({ 13 | value = '', 14 | onDateChange, 15 | disabled = false, 16 | min = null, 17 | max = null, 18 | id, 19 | }) => { 20 | const [currentValue, setCurrentValue] = useState(value ?? ''); 21 | const inputRef = useRef(null); 22 | 23 | useEffect(() => { 24 | setCurrentValue(value ?? ''); 25 | }, [value]); 26 | 27 | useEffect(() => { 28 | if (inputRef.current) { 29 | if (min !== null) { 30 | inputRef.current.setAttribute('min', min); 31 | } 32 | if (max !== null) { 33 | inputRef.current.setAttribute('max', max); 34 | } 35 | } 36 | }, [min, max]); 37 | 38 | const handleChange = (e: React.ChangeEvent) => { 39 | if (e.target.value.length > 0) { 40 | setCurrentValue(e.target.value); 41 | onDateChange?.(e.target.value); 42 | } 43 | }; 44 | 45 | return ( 46 | { 55 | e.preventDefault(); 56 | }} 57 | /> 58 | ); 59 | }; 60 | 61 | export { DatePicker }; 62 | -------------------------------------------------------------------------------- /src/webview/components/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/strict-boolean-expressions */ 2 | import React from 'react'; 3 | 4 | interface ICheckBox { 5 | children: React.ReactNode; 6 | checked?: boolean; 7 | onChange?: (event: React.ChangeEvent) => void; 8 | disabled?: boolean; 9 | } 10 | 11 | const ContrastCheckbox = ({ 12 | children, 13 | checked, 14 | onChange, 15 | disabled = false, 16 | }: ICheckBox) => { 17 | return ( 18 | 50 | ); 51 | }; 52 | 53 | export { ContrastCheckbox }; 54 | -------------------------------------------------------------------------------- /src/vscode-extension/utils/treeItems.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { 3 | CONSTRAST_ABOUT, 4 | CONSTRAST_ASSESS, 5 | CONSTRAST_SCAN, 6 | CONSTRAST_SETTING, 7 | } from './constants/commands'; 8 | import { TreeItem, Uri } from 'vscode'; 9 | import { localeI18ln } from '../../l10n'; 10 | 11 | class ContrastTreeItem extends TreeItem { 12 | constructor(label: string, command: string = '', icon: string | null = null) { 13 | super(label); 14 | if (command !== null) { 15 | this.command = { 16 | title: label, 17 | command: command, 18 | }; 19 | } 20 | if (icon !== null && icon !== undefined) { 21 | const projectRoot = path.resolve(__dirname, '..'); 22 | const iconPath = Uri.file(path.join(projectRoot, 'assets', icon)); 23 | this.iconPath = { 24 | dark: iconPath, 25 | light: iconPath, 26 | }; 27 | } 28 | } 29 | } 30 | 31 | const ContrastNavigationItems: ContrastTreeItem[] = [ 32 | new ContrastTreeItem( 33 | localeI18ln.getTranslation('contrastItems.about') as string, 34 | CONSTRAST_ABOUT, 35 | 'information.png' 36 | ), 37 | new ContrastTreeItem( 38 | localeI18ln.getTranslation('contrastItems.settings') as string, 39 | CONSTRAST_SETTING, 40 | 'settings.png' 41 | ), 42 | new ContrastTreeItem( 43 | localeI18ln.getTranslation('contrastItems.scan') as string, 44 | CONSTRAST_SCAN, 45 | 'contrast-scan.png' 46 | ), 47 | new ContrastTreeItem( 48 | localeI18ln.getTranslation('contrastItems.assess') as string, 49 | CONSTRAST_ASSESS, 50 | 'contrast-assess.png' 51 | ), 52 | ]; 53 | 54 | export { ContrastNavigationItems, ContrastTreeItem }; 55 | -------------------------------------------------------------------------------- /src/webview/screens/Scan/tabs/CurrentFile/CurrentFileVul.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { FC, useEffect } from 'react'; 3 | import { VulnerabilityReport } from '../../../../components/Scan/Vulnerability/VulnerabilityReport'; 4 | import { webviewPostMessage } from '../../../../utils/postMessage'; 5 | import { 6 | WEBVIEW_COMMANDS, 7 | WEBVIEW_SCREENS, 8 | } from '../../../../../vscode-extension/utils/constants/commands'; 9 | import { useSelector } from 'react-redux'; 10 | import { 11 | ProjectVulnerability, 12 | ReducerTypes, 13 | } from '../../../../../common/types'; 14 | 15 | const CurrentFileVul: FC = () => { 16 | const getCurrentFileVulFromState = useSelector( 17 | (state: ReducerTypes) => state.vulnerability.currentFile 18 | ); 19 | const [fileVul, setFileVul] = useState([]); 20 | 21 | useEffect(() => { 22 | webviewPostMessage({ 23 | command: WEBVIEW_COMMANDS.SCAN_GET_CURRENTFILE_VUL, 24 | payload: null, 25 | screen: WEBVIEW_SCREENS.SCAN, 26 | }); 27 | }, []); 28 | 29 | useEffect(() => { 30 | if (getCurrentFileVulFromState?.code === 200) { 31 | setFileVul( 32 | getCurrentFileVulFromState.responseData as ProjectVulnerability[] 33 | ); 34 | } else { 35 | setFileVul([]); 36 | } 37 | }, [getCurrentFileVulFromState]); 38 | 39 | return ( 40 | <> 41 | {fileVul.length === 0 ? ( 42 | <> 43 |
44 |

{getCurrentFileVulFromState?.message}

45 |
46 | 47 | ) : ( 48 | 49 | )} 50 | 51 | ); 52 | }; 53 | 54 | export { CurrentFileVul }; 55 | -------------------------------------------------------------------------------- /src/test/unitTest/webview/CurrentFile.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { useSelector } from 'react-redux'; 3 | import { webviewPostMessage } from '../../../webview/utils/postMessage'; 4 | import { CurrentFileVul } from '../../../webview/screens/Scan/tabs/CurrentFile/CurrentFileVul'; 5 | import { 6 | WEBVIEW_COMMANDS, 7 | WEBVIEW_SCREENS, 8 | } from '../../../vscode-extension/utils/constants/commands'; 9 | 10 | jest.mock('react-redux', () => ({ 11 | useSelector: jest.fn(), 12 | })); 13 | 14 | jest.mock('../../../webview/utils/postMessage', () => ({ 15 | webviewPostMessage: jest.fn(), 16 | })); 17 | 18 | describe('CurrentFileVul Component', () => { 19 | beforeEach(() => { 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | test('should send a postMessage command to get current file vulnerabilities on mount', () => { 24 | (useSelector as unknown as jest.Mock).mockReturnValueOnce(null); 25 | 26 | render(); 27 | 28 | expect(webviewPostMessage).toHaveBeenCalledWith({ 29 | command: WEBVIEW_COMMANDS.SCAN_GET_CURRENTFILE_VUL, 30 | payload: null, 31 | screen: WEBVIEW_SCREENS.SCAN, 32 | }); 33 | }); 34 | 35 | test('should not render the VulnerabilityReport when no vulnerabilities are available', () => { 36 | (useSelector as unknown as jest.Mock).mockReturnValue({ 37 | code: 404, 38 | responseData: [], 39 | }); 40 | 41 | render(); 42 | 43 | expect(webviewPostMessage).toHaveBeenCalledWith({ 44 | command: 'getCurrentFileVul', 45 | payload: null, 46 | screen: 'CONFIGURE_SCAN', 47 | }); 48 | 49 | expect(screen.queryByText('Found 6 issues in 3 file')).toBeNull(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/vscode-extension/commands/ui-commands/retrieveVulnerabilityCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | CONSTRAST_SCAN, 4 | CONTRAST_RETRIEVE_VULNERABILITIES, 5 | WEBVIEW_COMMANDS, 6 | } from '../../utils/constants/commands'; 7 | import { getProjectVulnerabilties } from '../../api/services/apiService'; 8 | import { ContrastPanelInstance } from './webviewHandler'; 9 | import { toggleContrastPanel } from '../../utils/toggleContrastPanel'; 10 | import { ShowInformationPopup } from './messageHandler'; 11 | import { localeI18ln } from '../../../l10n'; 12 | import { 13 | featureController, 14 | interlockModeSwitch, 15 | scanRetrieveBlocker, 16 | } from '../../utils/helper'; 17 | import { scanRetrievelDetectorAcrossIds } from '../../utils/multiInstanceConfigSync'; 18 | 19 | const registerRetrieveVulCommand = vscode.commands.registerCommand( 20 | CONTRAST_RETRIEVE_VULNERABILITIES, 21 | async () => { 22 | const isModeSwitched = await interlockModeSwitch('scan'); 23 | const scanRetrieveBlockerDetect = scanRetrieveBlocker.getSlot(); 24 | if (isModeSwitched && scanRetrieveBlockerDetect === false) { 25 | featureController.setSlot('scan'); 26 | ShowInformationPopup( 27 | localeI18ln.getTranslation('persistResponse.retrievingVulnerabilities') 28 | ); 29 | await scanRetrievelDetectorAcrossIds.disable(); 30 | const res = await getProjectVulnerabilties(); 31 | await scanRetrievelDetectorAcrossIds.enable(); 32 | vscode.commands.executeCommand(CONSTRAST_SCAN); 33 | toggleContrastPanel(); 34 | ContrastPanelInstance.activeRetrieveVulnerability(); 35 | ContrastPanelInstance.postMessage({ 36 | command: WEBVIEW_COMMANDS.SCAN_GET_ALL_FILES_VULNERABILITY, 37 | data: res, 38 | }); 39 | } 40 | } 41 | ); 42 | 43 | export { registerRetrieveVulCommand }; 44 | -------------------------------------------------------------------------------- /src/test/unitTest/vscode-extension/encryptDecrypt.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { SecretService } from '../../../vscode-extension/utils/encryptDecrypt'; 3 | import * as vscode from 'vscode'; 4 | 5 | describe('SecretService', () => { 6 | let mockSecretStorage: jest.Mocked; 7 | let service: SecretService; 8 | 9 | beforeEach(() => { 10 | mockSecretStorage = { 11 | store: jest.fn(), 12 | get: jest.fn(), 13 | delete: jest.fn(), 14 | onDidChange: jest.fn() as any, // required by the interface, but not used 15 | }; 16 | service = new SecretService(mockSecretStorage); 17 | }); 18 | 19 | test('storeSecret should call secretStorage.store with key and value', async () => { 20 | await service.storeSecret('testKey', 'testValue'); 21 | expect(mockSecretStorage.store).toHaveBeenCalledWith( 22 | 'testKey', 23 | 'testValue' 24 | ); 25 | }); 26 | 27 | test('getSecret should call secretStorage.get and return the value', async () => { 28 | mockSecretStorage.get.mockResolvedValue('storedValue'); 29 | 30 | const result = await service.getSecret('testKey'); 31 | expect(mockSecretStorage.get).toHaveBeenCalledWith('testKey'); 32 | expect(result).toBe('storedValue'); 33 | }); 34 | 35 | test('getSecret should return undefined if key not found', async () => { 36 | mockSecretStorage.get.mockResolvedValue(undefined); 37 | 38 | const result = await service.getSecret('missingKey'); 39 | expect(mockSecretStorage.get).toHaveBeenCalledWith('missingKey'); 40 | expect(result).toBeUndefined(); 41 | }); 42 | 43 | test('deleteSecret should call secretStorage.delete with key', async () => { 44 | await service.deleteSecret('testKey'); 45 | expect(mockSecretStorage.delete).toHaveBeenCalledWith('testKey'); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/test/unitTest/vscode-extension/cacheLogger.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | import { logInfo } from '../../../vscode-extension/logging/cacheLogger'; 5 | 6 | jest.mock('fs'); 7 | jest.mock('axios'); 8 | jest.mock('vscode', () => ({ 9 | env: { 10 | language: 'en', 11 | appName: 'VSCode', 12 | }, 13 | workspace: { 14 | workspaceFolders: [{ uri: { fsPath: '/path/to/mock/workspace' } }], 15 | }, 16 | })); 17 | 18 | describe('Logging functionality', () => { 19 | const today = new Date(); 20 | const logFilePath = path.join( 21 | '/path/to/mock/workspace', 22 | 'application', 23 | `log_${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}.txt` 24 | ); 25 | const startTime = new Date('2024-11-01T12:00:00Z'); 26 | const endTime = new Date('2024-11-01T12:05:00Z'); 27 | const message = 'Test log message'; 28 | 29 | beforeEach(() => { 30 | jest.clearAllMocks(); 31 | }); 32 | 33 | it('should create the log file directory if it does not exist', () => { 34 | (fs.existsSync as jest.Mock).mockReturnValue(false); 35 | 36 | logInfo(startTime, endTime, message); 37 | 38 | expect(fs.mkdirSync).toHaveBeenCalledWith(path.dirname(logFilePath), { 39 | recursive: true, 40 | }); 41 | }); 42 | 43 | it('should append log data to the log file', () => { 44 | (fs.existsSync as jest.Mock).mockReturnValue(true); 45 | 46 | (fs.appendFileSync as jest.Mock).mockImplementation(() => {}); 47 | 48 | logInfo(startTime, endTime, message); 49 | 50 | const expectedLogData = ` 51 | Start Time: 2024-11-01T12:00:00.000Z 52 | End Time: 2024-11-01T12:05:00.000Z 53 | Message: Test log message 54 | `; 55 | expect(fs.appendFileSync).toHaveBeenCalledWith( 56 | logFilePath, 57 | expectedLogData + '\n', 58 | 'utf8' 59 | ); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/webview/utils/redux/slices/projectsSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { ProjectState } from '../../../../common/types'; 3 | 4 | const projectState: ProjectState = { 5 | configuredProjects: null, 6 | allProjectList: null, 7 | allApplicationList: null, 8 | configuredProjectDelete: null, 9 | addConfigureProject: null, 10 | updateConfigureProject: null, 11 | cancelStateWhileDelete: false, 12 | settingActions: false, 13 | }; 14 | 15 | const projectSlice = createSlice({ 16 | name: 'project', 17 | initialState: projectState, 18 | reducers: { 19 | getAllConfiguredProjects: (state, action) => { 20 | state.configuredProjects = action.payload; 21 | }, 22 | getAllProjectList: (state, action) => { 23 | state.allProjectList = action.payload; 24 | }, 25 | // New 26 | getAllApplicationList: (state, action) => { 27 | state.allApplicationList = action.payload; 28 | }, 29 | updateConfiguredProjectDelete: (state, action) => { 30 | state.configuredProjectDelete = action.payload; 31 | }, 32 | addConfigureProject: (state, action) => { 33 | state.addConfigureProject = action.payload; 34 | }, 35 | updateConfigureProject: (state, action) => { 36 | state.updateConfigureProject = action.payload; 37 | }, 38 | setCancelStateWhileDelete: (state, action) => { 39 | state.cancelStateWhileDelete = action.payload; 40 | }, 41 | setSettingActions: (state, action) => { 42 | state.settingActions = action.payload; 43 | }, 44 | }, 45 | }); 46 | 47 | export const { 48 | getAllConfiguredProjects, 49 | getAllProjectList, 50 | getAllApplicationList, 51 | updateConfiguredProjectDelete, 52 | addConfigureProject, 53 | updateConfigureProject, 54 | setCancelStateWhileDelete, 55 | setSettingActions, 56 | } = projectSlice.actions; 57 | const ProjectReducer = projectSlice.reducer; 58 | 59 | export { ProjectReducer }; 60 | -------------------------------------------------------------------------------- /src/webview/utils/redux/slices/ScanFilter.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { ScanState } from '../../../../common/types'; 3 | 4 | const scanState: ScanState = { 5 | filters: null, 6 | activeProjectName: null, 7 | validWorkspaceProjects: [], 8 | backgroundVulnRunner: false, 9 | manualRefreshBackgroundVulnRunner: false, 10 | activeCurrentFile: null, 11 | activeVulnerabilityReport: null, 12 | scanRetrievelDetectAcrossIds: false, 13 | }; 14 | 15 | const scanSlice = createSlice({ 16 | name: 'scan', 17 | initialState: scanState, 18 | reducers: { 19 | getFilters: (state, action) => { 20 | state.filters = action.payload; 21 | }, 22 | setActiveProjectName: (state, action) => { 23 | state.activeProjectName = action.payload; 24 | }, 25 | setValidWorkspaceProjects: (state, action) => { 26 | state.validWorkspaceProjects = action.payload; 27 | }, 28 | setScanBackgroundRunner: (state, action) => { 29 | state.backgroundVulnRunner = action.payload; 30 | }, 31 | setScanManualRefreshBackgroundRunner: (state, action) => { 32 | state.manualRefreshBackgroundVulnRunner = action.payload; 33 | }, 34 | setActiveCurrentFile: (state, action) => { 35 | state.activeCurrentFile = action.payload; 36 | }, 37 | setVulnerabilityReport: (state, action) => { 38 | state.activeVulnerabilityReport = action.payload; 39 | }, 40 | setScanRetrievelDetectAcrossIds: (state, action) => { 41 | state.scanRetrievelDetectAcrossIds = action.payload; 42 | }, 43 | }, 44 | }); 45 | 46 | export const { 47 | getFilters, 48 | setActiveCurrentFile, 49 | setScanBackgroundRunner, 50 | setScanManualRefreshBackgroundRunner, 51 | setVulnerabilityReport, 52 | setScanRetrievelDetectAcrossIds, 53 | setActiveProjectName, 54 | setValidWorkspaceProjects, 55 | } = scanSlice.actions; 56 | const ScanReducer = scanSlice.reducer; 57 | 58 | export { ScanReducer }; 59 | -------------------------------------------------------------------------------- /webpack.webview.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | module.exports = { 4 | mode: 'development', // Use 'production' for production builds 5 | entry: './src/webview/index.tsx', // Entry point for your app 6 | output: { 7 | path: path.resolve(__dirname, 'out'), // Output directory 8 | filename: 'bundle.js', // Output bundle file 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|jsx|ts|tsx)$/, // Handle .js, .jsx, .ts, and .tsx files 14 | exclude: /node_modules|src\/test/, // Exclude dependencies 15 | use: { 16 | loader: 'babel-loader', 17 | options: { 18 | presets: [ 19 | '@babel/preset-env', // Transpile modern JavaScript (ES6+) 20 | '@babel/preset-react', // Transpile JSX for React 21 | '@babel/preset-typescript', // Transpile TypeScript 22 | ], 23 | }, 24 | }, 25 | }, 26 | { 27 | test: /\.scss$/, 28 | use: [ 29 | 'style-loader', // Injects styles into DOM 30 | 'css-loader', // Turns CSS into CommonJS 31 | 'sass-loader', // Compiles Sass to CSS 32 | ], 33 | }, 34 | { 35 | test: /\.css$/i, 36 | use: ['style-loader', 'css-loader'], 37 | }, 38 | { 39 | test: /\.(png|svg)$/, // Handle image files 40 | use: ['file-loader'], 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: ['.js', '.jsx', '.ts', '.tsx'], // Automatically resolve these extensions 46 | fallback: { 47 | 'process/browser': require.resolve('process/browser'), // Provide fallback for "process" 48 | buffer: require.resolve('buffer/'), 49 | }, 50 | }, 51 | plugins: [ 52 | new webpack.ProvidePlugin({ 53 | process: 'process/browser', // Make "process" globally available 54 | Buffer: ['buffer', 'Buffer'], 55 | }), 56 | ], 57 | }; 58 | -------------------------------------------------------------------------------- /src/vscode-extension/utils/persistanceState.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | AssessFilter, 4 | ConfiguredProject, 5 | FilterType, 6 | PersistedDTO, 7 | } from '../../common/types'; 8 | 9 | class PersistenceState { 10 | localStorage!: vscode.Memento; 11 | 12 | registerContext(context: vscode.Memento) { 13 | this.localStorage = context; 14 | } 15 | 16 | async set( 17 | token: string, 18 | key: keyof PersistedDTO, // Restrict the key to properties of PersistedDTO 19 | payload: FilterType | ConfiguredProject[] | AssessFilter 20 | ): Promise { 21 | const getTokenData: PersistedDTO = this.get(token) as PersistedDTO; 22 | 23 | if (getTokenData !== null) { 24 | // Ensure type safety while updating 25 | const internal: PersistedDTO = { 26 | ...getTokenData, 27 | [key]: payload as ConfiguredProject[] | FilterType, // Type assertion based on the key 28 | }; 29 | 30 | await this.localStorage.update(token, internal); 31 | return true; 32 | } 33 | 34 | // Create a new store if no data exists 35 | const store: PersistedDTO = { [key]: payload } as PersistedDTO; 36 | await this.localStorage.update(token, store); 37 | return true; 38 | } 39 | 40 | get(token: string): PersistedDTO { 41 | return this.localStorage.get(token) as PersistedDTO; 42 | } 43 | 44 | getByKey( 45 | token: string, 46 | key: keyof PersistedDTO 47 | ): FilterType | ConfiguredProject[] | null | undefined { 48 | const getTokenData: PersistedDTO = this.get(token) as PersistedDTO; 49 | 50 | if (getTokenData !== null && getTokenData !== undefined) { 51 | return getTokenData[key]; 52 | } 53 | return null; 54 | } 55 | 56 | async clear(token: string): Promise { 57 | await this.localStorage.update(token, null); 58 | } 59 | } 60 | 61 | const PersistenceInstance = new PersistenceState(); 62 | export { PersistenceState, PersistenceInstance }; 63 | -------------------------------------------------------------------------------- /src/webview/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, MouseEventHandler, ReactNode } from 'react'; 2 | import { Tooltip } from '@mui/material'; 3 | import PlayArrowOutlinedIcon from '@mui/icons-material/PlayArrowOutlined'; 4 | import { ButtonProps } from '../../common/types'; 5 | 6 | const getVariantClass = ( 7 | variant?: ButtonProps['variant'], 8 | className?: string 9 | ) => { 10 | const baseClassMap: Record = { 11 | run: 'run-action-button', 12 | }; 13 | 14 | return variant && baseClassMap[variant] 15 | ? `${baseClassMap[variant]} ${className ?? ''}`.trim() 16 | : `custom-button ${className ?? ''}`.trim(); 17 | }; 18 | 19 | const getVariantContent = ( 20 | variant?: ButtonProps['variant'], 21 | title?: string, 22 | isDisable?: boolean 23 | ): ReactNode => { 24 | switch (variant) { 25 | case 'run': 26 | return ( 27 |
28 | 29 | 33 | 34 | {title} 35 |
36 | ); 37 | default: 38 | return title; 39 | } 40 | }; 41 | 42 | const Button: FC = ({ 43 | onClick, 44 | title, 45 | color = '', 46 | className = '', 47 | isDisable = false, 48 | id, 49 | tooltip = '', 50 | variant, 51 | }) => { 52 | const buttonClassName = getVariantClass(variant, `${color} ${className}`); 53 | 54 | return ( 55 | 56 | 65 | 66 | ); 67 | }; 68 | 69 | export { Button }; 70 | -------------------------------------------------------------------------------- /src/test/mocks/testMock.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConfiguredProject, 3 | FileVulnerability, 4 | Vulnerability, 5 | } from '../../common/types'; 6 | 7 | const configuredProject1: ConfiguredProject = { 8 | id: '01', 9 | source: 'scan', 10 | contrastURL: 'https://xyz.com', 11 | userName: 'xyz@xyz.com', 12 | serviceKey: 'ABCDEFGHIJ', 13 | apiKey: 'PQRS1234TUV5678', 14 | organizationId: '123-XYZ-456-ABC-789', 15 | minute: '1440', 16 | projectName: 'Test Project', 17 | projectId: '456-ABC-789-XYZ', 18 | }; 19 | const configuredProject2: ConfiguredProject = { 20 | id: '02', 21 | source: 'scan', 22 | contrastURL: 'https://abc.com', 23 | userName: 'abc@abc.com', 24 | serviceKey: 'KLMNOPQRST', 25 | apiKey: 'PQRS1234TUV5678', 26 | organizationId: '456-XYZ-892-ABC-789', 27 | minute: '1440', 28 | projectName: 'Test Project 2', 29 | projectId: '789-EFG-456-XYZ', 30 | }; 31 | 32 | const childNode: Vulnerability = { 33 | level: 0, 34 | label: 'Replace this use of console.log', 35 | lineNumber: 123, 36 | popupMessage: { 37 | message: 'Incorrect way using fragments', 38 | lastDetected_date: '02-may-2007', 39 | status: 'pending', 40 | link: null, 41 | }, 42 | severity: 'low', 43 | }; 44 | 45 | const parentNode: FileVulnerability = { 46 | level: 1, 47 | label: 'app.component.ts', 48 | issuesCount: 2, 49 | filePath: 'src/app/app.component.ts', 50 | fileType: '.ts', 51 | child: [ 52 | { 53 | level: 0, 54 | label: 'Define a constant instead of duplicating', 55 | lineNumber: 123, 56 | popupMessage: { 57 | message: "fix the image size 123x23 'FIX'", 58 | lastDetected_date: '01-may-2007', 59 | status: 'completed', 60 | link: 'https://google.com', 61 | }, 62 | severity: 'high', 63 | }, 64 | { 65 | level: 0, 66 | label: 'Replace this use of console.log ', 67 | lineNumber: 123, 68 | popupMessage: { 69 | message: 'Incorrect way using fragments', 70 | lastDetected_date: '02-may-2007', 71 | status: 'pending', 72 | link: null, 73 | }, 74 | severity: 'low', 75 | }, 76 | ], 77 | }; 78 | 79 | export { configuredProject1, configuredProject2, childNode, parentNode }; 80 | -------------------------------------------------------------------------------- /src/vscode-extension/utils/localeMemoryCache.ts: -------------------------------------------------------------------------------- 1 | import { TOKEN } from './constants/commands'; 2 | import * as cacheManager from 'cache-manager'; 3 | 4 | // Factory to create an in-memory cache instance 5 | const createCacheInstance = () => { 6 | return cacheManager.caching({ 7 | store: 'memory', 8 | max: 100, 9 | ttl: 86400, // 1 day in seconds 10 | }); 11 | }; 12 | 13 | // Strongly typed cache store structure 14 | type CacheStore = Record; 15 | 16 | class LocaleMemoryCache { 17 | private readonly memory: ReturnType | null = 18 | createCacheInstance(); 19 | 20 | private isCacheAvailable(): boolean { 21 | return this.memory !== null && typeof this.memory.get === 'function'; 22 | } 23 | 24 | /** Get entire cached store for a token */ 25 | public async getStore(token: TOKEN): Promise { 26 | if (!this.isCacheAvailable()) { 27 | return null; 28 | } 29 | return (await this.memory!.get(token)) ?? null; 30 | } 31 | 32 | /** Get specific item by key from cached store */ 33 | public async getItem(token: TOKEN, key: string): Promise { 34 | if (!this.isCacheAvailable()) { 35 | return null; 36 | } 37 | const store = await this.memory!.get(token); 38 | return store?.[key] ?? null; 39 | } 40 | 41 | /** Set or update a specific item in the cached store */ 42 | public async setItem( 43 | token: TOKEN, 44 | key: string, 45 | value: unknown 46 | ): Promise { 47 | if (!this.isCacheAvailable()) { 48 | return; 49 | } 50 | const store = await this.memory!.get(token); 51 | const updatedStore: CacheStore = { 52 | ...store, 53 | [key]: value, 54 | }; 55 | await this.memory!.set(token, updatedStore); 56 | } 57 | 58 | /** Clear cache for the given token */ 59 | public async clearStore(token: TOKEN): Promise { 60 | if (!this.isCacheAvailable()) { 61 | return false; 62 | } 63 | await this.memory!.del(token); 64 | return true; 65 | } 66 | } 67 | 68 | const LocaleMemoryCacheInstance = new LocaleMemoryCache(); 69 | 70 | export { createCacheInstance, LocaleMemoryCache, LocaleMemoryCacheInstance }; 71 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | const CopyPlugin = require('copy-webpack-plugin'); 7 | const { default: axios } = require('axios'); 8 | 9 | //@ts-check 10 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 11 | 12 | /** @type WebpackConfig */ 13 | const extensionConfig = { 14 | target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 15 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 16 | 17 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 18 | output: { 19 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 20 | path: path.resolve(__dirname, 'dist'), 21 | filename: 'extension.js', 22 | libraryTarget: 'commonjs2', 23 | }, 24 | externals: { 25 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 26 | // modules added here also need to be added in the .vscodeignore file 27 | }, 28 | resolve: { 29 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 30 | extensions: ['.ts', '.js'], 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.ts$/, 36 | exclude: /node_modules|src\/test/, 37 | use: [ 38 | { 39 | loader: 'ts-loader', 40 | }, 41 | ], 42 | }, 43 | ], 44 | }, 45 | plugins: [ 46 | new CopyPlugin({ 47 | patterns: [ 48 | { 49 | from: 'node_modules/@vscode/webview-ui-toolkit/dist/toolkit.js', 50 | to: 'node_modules/@vscode/webview-ui-toolkit/dist/toolkit.js', 51 | }, 52 | // Add any additional assets you need to copy here 53 | ], 54 | }), 55 | ], 56 | devtool: 'nosources-source-map', 57 | infrastructureLogging: { 58 | level: 'log', // enables logging required for problem matchers 59 | }, 60 | }; 61 | module.exports = [extensionConfig]; 62 | -------------------------------------------------------------------------------- /src/webview/components/RadioGroup.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | interface RadioProps { 4 | id: string; 5 | value: string; 6 | optionValue: string | null; 7 | onChange: (value: string) => void; 8 | label: string; 9 | isDisabled?: boolean; 10 | } 11 | 12 | interface RadioGroupProps { 13 | onChange: (value: string) => void; 14 | data: { id: string; value: string; label: string; isDisabled?: boolean }[]; 15 | value: string; 16 | } 17 | 18 | function Radio({ 19 | id, 20 | value, 21 | optionValue, 22 | onChange, 23 | label, 24 | isDisabled, 25 | }: RadioProps) { 26 | const isChecked = optionValue === value; 27 | 28 | return ( 29 |
30 | 51 |
52 | ); 53 | } 54 | 55 | export function RadioGroup({ onChange, data, value }: RadioGroupProps) { 56 | const [radioValue, setRadioValue] = useState(''); 57 | 58 | const handleRadioChange = (evValue: string) => { 59 | setRadioValue(evValue); 60 | onChange(evValue); 61 | }; 62 | 63 | useEffect(() => { 64 | setRadioValue(value); 65 | onChange(value); 66 | }, [value]); 67 | 68 | return ( 69 |
70 | {data.map((item) => ( 71 | 80 | ))} 81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/webview/screens/Assess/tabs/Filters/AssessFilters.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | ContrastAssessLocale, 4 | ReducerTypes, 5 | } from '../../../../../common/types'; 6 | import { useSelector } from 'react-redux'; 7 | import { TabGroup } from '../../../../components/TabGroup'; 8 | import { Tab } from '../../../../components/Tab'; 9 | import AssessFilterComponent from './tabs/AssessFilterComponent'; 10 | import { LibraryFilterComponent } from './tabs/LibraryFilterComponent'; 11 | 12 | function TabViewer({ tabId }: { tabId: number }) { 13 | switch (tabId) { 14 | case 1: 15 | return ; 16 | case 2: 17 | return ; 18 | case 3: 19 | default: 20 | return null; 21 | } 22 | } 23 | 24 | // Main ContrastScan Component 25 | function AssessFilters() { 26 | const i18nData = useSelector((state: ReducerTypes) => state.i10ln.data); 27 | const [tabId, setTabId] = useState(1); 28 | const [tabs, setTabs] = useState([ 29 | { id: 1, title: 'Vulnerabilities', active: tabId === 1 }, 30 | { id: 2, title: 'Library', active: tabId === 2 }, 31 | ]); 32 | 33 | useEffect(() => { 34 | if (i18nData !== null && i18nData !== undefined) { 35 | const { filters } = i18nData as unknown as ContrastAssessLocale; 36 | setTabs([ 37 | { ...tabs[0], title: filters?.assess?.translate as string }, 38 | { ...tabs[1], title: filters.library?.translate as string }, 39 | ]); 40 | } 41 | }, [i18nData]); 42 | 43 | function handleTabChange(id: number) { 44 | setTabId(id); 45 | setTabs((prevTabs) => 46 | prevTabs.map((tab) => ({ 47 | ...tab, 48 | active: tab.id === id, 49 | })) 50 | ); 51 | } 52 | 53 | return ( 54 |
55 |
56 |
57 | 58 | {tabs.map((tab) => ( 59 | 60 | ))} 61 | 62 |
63 |
64 | 65 |
66 |
67 |
68 | ); 69 | } 70 | 71 | export { AssessFilters }; 72 | -------------------------------------------------------------------------------- /src/webview/screens/Assess/tabs/CurrentFile/AssessCurrentFile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { FC, useEffect } from 'react'; 3 | // import { VulnerabilityReport } from '../../../../components/Scan/Vulnerability/VulnerabilityReport'; 4 | import { webviewPostMessage } from '../../../../utils/postMessage'; 5 | import { 6 | WEBVIEW_COMMANDS, 7 | WEBVIEW_SCREENS, 8 | } from '../../../../../vscode-extension/utils/constants/commands'; 9 | import { useSelector } from 'react-redux'; 10 | import { 11 | ProjectVulnerability, 12 | ReducerTypes, 13 | } from '../../../../../common/types'; 14 | import { AssessVulnerabilityReport } from '../../../../components/Assess/VulnerabilityReport'; 15 | 16 | const AssessCurrentFile: FC = () => { 17 | const getCurrentFileVulFromState = useSelector( 18 | (state: ReducerTypes) => state.assessFilter.currentFile 19 | ); 20 | const [fileVul, setFileVul] = useState([]); 21 | 22 | useEffect(() => { 23 | webviewPostMessage({ 24 | command: WEBVIEW_COMMANDS.ASSESS_GET_CURRENTFILE_VUL, 25 | payload: null, 26 | screen: WEBVIEW_SCREENS.ASSESS, 27 | }); 28 | }, []); 29 | 30 | useEffect(() => { 31 | if (getCurrentFileVulFromState?.code === 200) { 32 | let data = getCurrentFileVulFromState.responseData; 33 | if (!Array.isArray(data)) { 34 | data = [data] as ProjectVulnerability[]; 35 | } 36 | setFileVul(data as ProjectVulnerability[]); 37 | } else { 38 | setFileVul([]); 39 | } 40 | }, [getCurrentFileVulFromState]); 41 | 42 | return ( 43 | <> 44 | {fileVul.length === 0 ? ( 45 | <> 46 |
47 |

{getCurrentFileVulFromState?.message}

48 |
49 | 50 | ) : ( 51 | { 54 | if (e !== undefined && e !== null) { 55 | webviewPostMessage({ 56 | command: WEBVIEW_COMMANDS.ASSESS_OPEN_VULNERABILITY_FILE, 57 | payload: e, 58 | screen: WEBVIEW_SCREENS.ASSESS, 59 | }); 60 | } 61 | }} 62 | /> 63 | )} 64 | 65 | ); 66 | }; 67 | 68 | export { AssessCurrentFile }; 69 | -------------------------------------------------------------------------------- /src/test/unitTest/webview/Events.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import { 4 | Events, 5 | PaneHeader, 6 | TreeRow, 7 | TreeView, 8 | } from '../../../webview/screens/Assess/tabs/VulnerabilityReport/tabs/Events'; 9 | 10 | jest.mock('../../../webview/utils/postMessage', () => ({ 11 | webviewPostMessage: jest.fn(), 12 | })); 13 | 14 | describe('PaneHeader Component', () => { 15 | const mockOnClick = jest.fn(); 16 | const nodeMock = { 17 | child: [], 18 | label: 'Test Node', 19 | type: 'Creation', 20 | }; 21 | 22 | test('renders PaneHeader with label', () => { 23 | render(); 24 | expect(screen.getByText('Test Node')).toBeInTheDocument(); 25 | }); 26 | 27 | test('renders warning icon with correct color', () => { 28 | render(); 29 | const warningIcon = screen.getByTestId('warning-icon'); 30 | expect(warningIcon).toBeInTheDocument(); 31 | expect(warningIcon).toHaveStyle('color: #F78A31'); 32 | }); 33 | 34 | test('calls onClick when arrow icon is clicked', () => { 35 | const nodeWithChildren = { ...nodeMock, child: [{}] }; 36 | render( 37 | 38 | ); 39 | const arrowIcon = screen.getByRole('pane').querySelector('i'); 40 | fireEvent.click(arrowIcon!); 41 | expect(mockOnClick).toHaveBeenCalledTimes(1); 42 | }); 43 | }); 44 | 45 | describe('TreeRow Component', () => { 46 | const nodeMock = { 47 | child: [], 48 | label: 'Test Node', 49 | type: 'Creation', 50 | isRoot: true, 51 | }; 52 | 53 | test('renders TreeRow and expands when clicked', () => { 54 | render(); 55 | const paneHeader = screen.getByText('Test Node'); 56 | fireEvent.click(paneHeader); 57 | expect(screen.getByText('Test Node')).toBeInTheDocument(); 58 | }); 59 | }); 60 | 61 | describe('TreeView Component', () => { 62 | const treeMock = [{ child: [], label: 'Node 1', type: 'Creation' }]; 63 | 64 | test('renders TreeView correctly', () => { 65 | render(); 66 | expect(screen.getByText('Node 1')).toBeInTheDocument(); 67 | }); 68 | }); -------------------------------------------------------------------------------- /src/webview/utils/vulMock.ts: -------------------------------------------------------------------------------- 1 | import { ProjectVulnerability } from '../../common/types'; 2 | 3 | const TREEDATA: ProjectVulnerability[] = [ 4 | { 5 | level: 2, 6 | label: 'Found 6 issues in 3 file', 7 | issuesCount: 10, 8 | filesCount: 3, 9 | child: [ 10 | { 11 | level: 1, 12 | label: 'app.component.ts', 13 | issuesCount: 2, 14 | filePath: 'src/app/app.component.ts', 15 | fileType: '.ts', 16 | child: [ 17 | { 18 | level: 0, 19 | label: 'Define a constant instead of duplicating ', 20 | lineNumber: 12, 21 | popupMessage: { 22 | message: '', 23 | lastDetected_date: '01-may-2007', 24 | status: 'completed', 25 | link: 'http://google.com', 26 | }, 27 | severity: 'high', 28 | }, 29 | { 30 | level: 0, 31 | label: 'Replace this use of console.log ', 32 | lineNumber: 123, 33 | popupMessage: { 34 | message: '', 35 | lastDetected_date: '02-may-2007', 36 | status: 'pending', 37 | link: null, 38 | }, 39 | severity: 'low', 40 | }, 41 | ], 42 | }, 43 | { 44 | level: 1, 45 | label: 'home.component.ts', 46 | issuesCount: 2, 47 | filePath: 'src/app/home.component.html', 48 | fileType: '.ts', 49 | child: [ 50 | { 51 | level: 0, 52 | label: "Replace this use of '
'", 53 | lineNumber: 23, 54 | popupMessage: { 55 | message: '', 56 | lastDetected_date: '01-may-2007', 57 | status: 'completed', 58 | link: 'http://mdn.com', 59 | }, 60 | severity: 'medium', 61 | }, 62 | { 63 | level: 0, 64 | label: 'Remove the use of nonce in Script tag', 65 | lineNumber: 232, 66 | popupMessage: { 67 | message: '', 68 | lastDetected_date: '30-may-2007', 69 | status: 'pending', 70 | link: 'http://google.com', 71 | }, 72 | severity: 'high', 73 | }, 74 | ], 75 | }, 76 | ], 77 | }, 78 | ]; 79 | 80 | export { TREEDATA }; 81 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 2 | import tsParser from '@typescript-eslint/parser'; 3 | import reactPlugin from 'eslint-plugin-react'; 4 | import prettierPlugin from 'eslint-plugin-prettier'; 5 | 6 | export default [ 7 | { 8 | files: ['**/*.ts', '**/*.tsx'], 9 | plugins: { 10 | '@typescript-eslint': typescriptEslint, 11 | react: reactPlugin, 12 | prettier: prettierPlugin, 13 | }, 14 | languageOptions: { 15 | parser: tsParser, 16 | parserOptions: { 17 | ecmaVersion: 2022, 18 | sourceType: 'module', 19 | project: './tsconfig.json', // Ensure this path matches your `tsconfig.json` location 20 | tsconfigRootDir: process.cwd(), // Set the root directory to the current working directory 21 | }, 22 | }, 23 | rules: { 24 | // TypeScript-specific rules 25 | '@typescript-eslint/naming-convention': [ 26 | 'warn', 27 | { 28 | selector: 'import', 29 | format: ['camelCase', 'PascalCase'], 30 | }, 31 | { 32 | selector: 'variable', 33 | format: ['camelCase', 'UPPER_CASE', 'PascalCase'], 34 | }, 35 | { 36 | selector: 'function', 37 | format: ['camelCase', 'PascalCase'], 38 | }, 39 | ], 40 | '@typescript-eslint/no-unused-vars': [ 41 | 'error', 42 | { argsIgnorePattern: '^_' }, 43 | ], 44 | '@typescript-eslint/no-explicit-any': 'error', 45 | '@typescript-eslint/no-unsafe-member-access': 'warn', 46 | '@typescript-eslint/strict-boolean-expressions': 'error', 47 | '@typescript-eslint/no-floating-promises': 'error', 48 | // React-specific rules 49 | 'react/jsx-uses-react': 'off', 50 | 'react/react-in-jsx-scope': 'off', 51 | 'react/prop-types': 'off', 52 | 53 | // General JavaScript rules 54 | curly: 'error', 55 | eqeqeq: ['error', 'always'], 56 | 'no-throw-literal': 'error', 57 | semi: ['error', 'always'], 58 | 'no-console': ['warn', { allow: ['warn', 'error'] }], 59 | 'no-var': 'error', 60 | 'prefer-const': 'error', 61 | 'arrow-spacing': ['error', { before: true, after: true }], 62 | 63 | // Prettier rules 64 | 'prettier/prettier': 'error', 65 | }, 66 | settings: { 67 | react: { 68 | version: 'detect', 69 | }, 70 | }, 71 | }, 72 | ]; 73 | -------------------------------------------------------------------------------- /src/l10n.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import * as fs from 'fs'; 3 | import { EXTENTION_COMMANDS } from './vscode-extension/utils/constants/commands'; 4 | import { LOCAL_LANG, LocalizationJSON, ScreenId } from './common/types'; 5 | import { env } from 'vscode'; 6 | 7 | class l10n { 8 | private setLocale: LocalizationJSON | null; 9 | constructor(lang: string) { 10 | this.setLocale = this.registerLocalization(this.validateLang(lang)); 11 | } 12 | 13 | private readonly validateLang = (lang: string): LOCAL_LANG => { 14 | switch (lang) { 15 | case LOCAL_LANG.ENGLISH: 16 | return LOCAL_LANG.ENGLISH; 17 | case LOCAL_LANG.JAPAN: 18 | return LOCAL_LANG.JAPAN; 19 | default: 20 | return LOCAL_LANG.ENGLISH; 21 | } 22 | }; 23 | 24 | private readonly registerLocalization = ( 25 | language: LOCAL_LANG 26 | ): LocalizationJSON => { 27 | const filePath = path.join( 28 | __dirname, 29 | '..', 30 | 'src', 31 | 'localization', 32 | `${language}.json` 33 | ); 34 | if (!fs.existsSync(filePath)) { 35 | throw new Error(`Localization file not found: ${filePath}`); 36 | } 37 | const rawData = fs.readFileSync(filePath, 'utf-8'); 38 | return JSON.parse(rawData) as LocalizationJSON; 39 | }; 40 | 41 | public changeLang(lang: LOCAL_LANG) { 42 | this.setLocale = this.registerLocalization(this.validateLang(lang)); 43 | } 44 | 45 | public getLocalization(screenId: ScreenId) { 46 | switch (screenId) { 47 | case EXTENTION_COMMANDS.SETTING_SCREEN: { 48 | return this.setLocale?.['contrastSettings']; 49 | } 50 | case EXTENTION_COMMANDS.SCAN_SCREEN: { 51 | return this.setLocale?.['contrastScan']; 52 | } 53 | case EXTENTION_COMMANDS.ASSESS_SCREEN: { 54 | return this.setLocale?.['contrastAssess']; 55 | } 56 | default: 57 | return null; 58 | } 59 | } 60 | 61 | public getTranslation(keyPath: string): string | undefined { 62 | const keys = keyPath.split('.'); 63 | /* eslint-disable @typescript-eslint/no-explicit-any */ 64 | let current: any = this.setLocale; 65 | for (const key of keys) { 66 | if (current[key] === undefined) { 67 | return undefined; 68 | } 69 | current = current[key]; 70 | } 71 | return current?.translate; 72 | } 73 | } 74 | 75 | const localeI18ln = new l10n(env.language); 76 | export { l10n, localeI18ln }; 77 | -------------------------------------------------------------------------------- /src/test/unitTest/vscode-extension/getScanResultProjectFailure.test.ts: -------------------------------------------------------------------------------- 1 | import { l10n } from '../../../l10n'; 2 | import { resolveFailure } from '../../../vscode-extension/utils/errorHandling'; 3 | import { PersistenceInstance } from '../../../vscode-extension/utils/persistanceState'; 4 | import { GetAllConfiguredProjects } from '../../../vscode-extension/persistence/PersistenceConfigSetting'; 5 | import { configuredProject1, configuredProject2 } from '../../mocks/testMock'; 6 | 7 | jest.mock('axios'); 8 | jest.mock('../../../vscode-extension/utils/errorHandling'); 9 | jest.mock('../../../vscode-extension/utils/persistanceState'); 10 | jest.mock('../../../vscode-extension/logging/cacheLogger'); 11 | jest.mock('../../../vscode-extension/cache/backgroundRefreshTimer'); 12 | jest.mock('../../../l10n'); 13 | 14 | const mockedResolveFailure = resolveFailure as jest.MockedFunction< 15 | typeof resolveFailure 16 | >; 17 | 18 | const locale = new l10n('en'); 19 | 20 | jest.mock('../../../vscode-extension/utils/persistanceState', () => ({ 21 | PersistenceInstance: { 22 | set: jest.fn(), 23 | getByKey: jest.fn(), 24 | }, 25 | })); 26 | 27 | jest.mock('../../../vscode-extension/utils/encryptDecrypt', () => ({ 28 | encrypt: jest.fn((key) => `encrypted-${key}`), 29 | })); 30 | 31 | jest.mock( 32 | '../../../vscode-extension/persistence/PersistenceConfigSetting', 33 | () => { 34 | return { 35 | GetAllConfiguredProjects: jest.fn(), 36 | }; 37 | } 38 | ); 39 | 40 | describe('getScanResultProject', () => { 41 | it('should return project not found if the project does not exist', async () => { 42 | const mockProjectData = [configuredProject1, configuredProject2]; 43 | /* eslint-disable @typescript-eslint/no-explicit-any */ 44 | const mockResponse: any = { 45 | responseData: mockProjectData, 46 | code: 200, 47 | status: 'success', 48 | message: 'Projects fetched successfully', 49 | }; 50 | ( 51 | GetAllConfiguredProjects as jest.MockedFunction< 52 | typeof GetAllConfiguredProjects 53 | > 54 | ).mockResolvedValueOnce(mockResponse); 55 | 56 | (PersistenceInstance.getByKey as jest.Mock).mockReturnValue( 57 | mockProjectData 58 | ); 59 | 60 | mockedResolveFailure.mockReturnValue({ 61 | message: locale.getTranslation('apiResponse.projectNotFound') as string, 62 | code: 400, 63 | status: 'failure', 64 | responseData: {}, 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/vscode-extension/commands/ui-commands/messageHandler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { WEBVIEW_COMMANDS } from '../../utils/constants/commands'; 4 | import { 5 | ApiResponse, 6 | CommandResponse, 7 | LocaleJson, 8 | } from '../../../common/types'; 9 | import { localeI18ln } from '../../../l10n'; 10 | 11 | function ShowErrorPopup(message: string) { 12 | vscode.window.showErrorMessage(message); 13 | } 14 | 15 | function ShowInformationPopup(message: string | undefined) { 16 | return vscode.window.showInformationMessage( 17 | message !== null && message !== undefined ? message : '' 18 | ); 19 | } 20 | 21 | function ShowInformationPopupWithOptions(message: string) { 22 | return vscode.window.showInformationMessage(message, 'Yes', 'no'); 23 | } 24 | 25 | function messageHandler(res: CommandResponse) { 26 | const messageCommands = [ 27 | WEBVIEW_COMMANDS.SCAN_MANUAL_REFRESH, 28 | WEBVIEW_COMMANDS.SETTING_ADD_PROJECT_TO_CONFIGURE, 29 | WEBVIEW_COMMANDS.SETTING_GET_ALL_PROJECTS, 30 | WEBVIEW_COMMANDS.SETTING_UPDATE_CONFIGURE_PROJECT, 31 | WEBVIEW_COMMANDS.SETTING_DELETE_CONFIGURE_PROJECT, 32 | WEBVIEW_COMMANDS.ASSESS_MANUAL_REFRESH, 33 | ]; 34 | if ( 35 | messageCommands.includes(res.command as WEBVIEW_COMMANDS) && 36 | isApiResponse(res.data) 37 | ) { 38 | if (res.data.status === 'failure') { 39 | ShowErrorPopup(res.data.message); 40 | return true; 41 | } else { 42 | const { responseData, code } = res.data; 43 | const response = responseData as { child: Array }; 44 | if ( 45 | (res.command === WEBVIEW_COMMANDS.SCAN_MANUAL_REFRESH || 46 | res.command === WEBVIEW_COMMANDS.ASSESS_MANUAL_REFRESH) && 47 | code === 200 && 48 | responseData !== null && 49 | responseData !== undefined && 50 | response.child.length === 0 51 | ) { 52 | ShowInformationPopup( 53 | localeI18ln.getTranslation( 54 | 'persistResponse.vulnerabilityNotFoundForFilters' 55 | ) 56 | ); 57 | return true; 58 | } 59 | ShowInformationPopup(res.data.message); 60 | } 61 | } 62 | return true; 63 | } 64 | 65 | // Type guard function to check if data is ApiResponse 66 | function isApiResponse( 67 | data: ApiResponse | LocaleJson | null | {} | undefined 68 | ): data is ApiResponse { 69 | return (data as ApiResponse)?.message !== undefined; 70 | } 71 | 72 | export { 73 | messageHandler, 74 | ShowInformationPopup, 75 | ShowInformationPopupWithOptions, 76 | ShowErrorPopup, 77 | }; 78 | -------------------------------------------------------------------------------- /src/vscode-extension/utils/statusBarSeverity.ts: -------------------------------------------------------------------------------- 1 | import { StatusBarItem, window, StatusBarAlignment, commands } from 'vscode'; 2 | import { ContrastPanelInstance } from '../commands/ui-commands/webviewHandler'; 3 | import { 4 | CONSTRAST_ASSESS, 5 | CONSTRAST_SCAN, 6 | CONTRAST_STATUSBAR_CLICK, 7 | } from './constants/commands'; 8 | import { featureController } from './helper'; 9 | 10 | // --------------- Global variables ----------------------- 11 | let statusBarItem: StatusBarItem; 12 | 13 | // Status Methods ------------------------------ 14 | 15 | // Initialize StatusBarItem 16 | function initializeStatusBarItem() { 17 | if (statusBarItem === undefined || statusBarItem === null) { 18 | statusBarItem = window.createStatusBarItem(StatusBarAlignment.Right); 19 | statusBarItem.text = 'Critical: 0 High: 0 Medium: 0 Low: 0'; // Default values 20 | statusBarItem.show(); 21 | statusBarItem.command = CONTRAST_STATUSBAR_CLICK; 22 | statusBarItem.command; 23 | } 24 | } 25 | 26 | // Function to update StatusBarItem with severity counts 27 | function updateStatusBarItem( 28 | critical: number, 29 | high: number, 30 | medium: number, 31 | low: number, 32 | note: number 33 | ) { 34 | statusBarItem.text = `Critical: ${critical} High: ${high} Medium: ${medium} Low: ${low} Note: ${note}`; 35 | statusBarItem.show(); // Ensure it's visible 36 | } 37 | 38 | function closeStatusBarItem() { 39 | if (statusBarItem !== undefined && statusBarItem !== null) { 40 | statusBarItem.hide(); 41 | } 42 | } 43 | 44 | const registerStatusBarCommend = commands.registerCommand( 45 | CONTRAST_STATUSBAR_CLICK, 46 | async () => { 47 | if (ContrastPanelInstance?.resolveWebviewView !== null) { 48 | if (featureController.getSlot() !== 'none') { 49 | switch (featureController.getSlot()) { 50 | case 'scan': 51 | { 52 | commands.executeCommand(CONSTRAST_SCAN); 53 | await new Promise((resolve) => setTimeout(resolve, 300)); 54 | ContrastPanelInstance.activeCurrentFile(); 55 | } 56 | break; 57 | case 'assess': 58 | { 59 | commands.executeCommand(CONSTRAST_ASSESS); 60 | await new Promise((resolve) => setTimeout(resolve, 300)); 61 | ContrastPanelInstance.assessActiveCurrentFile(); 62 | } 63 | break; 64 | } 65 | } 66 | } 67 | } 68 | ); 69 | 70 | export { 71 | initializeStatusBarItem, 72 | updateStatusBarItem, 73 | closeStatusBarItem, 74 | registerStatusBarCommend, 75 | }; 76 | -------------------------------------------------------------------------------- /src/vscode-extension/utils/listofAllVulnerabilities.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, window } from 'vscode'; 2 | import { openVulFile } from './vulnerabilityDecorator'; 3 | import { 4 | getAssessVulnerabilitybyFile, 5 | getVulnerabilitybyFile, 6 | } from '../api/services/apiService'; 7 | import { featureController, getFilePathuri, isNotNull } from './helper'; 8 | import { localeI18ln } from '../../l10n'; 9 | import { closeStatusBarItem } from './statusBarSeverity'; 10 | import { CustomFileVulnerability } from '../../common/types'; 11 | import path from 'path'; 12 | 13 | async function getFileFromCache( 14 | filePath: string 15 | ): Promise { 16 | const res = await getVulnerabilitybyFile(filePath); 17 | 18 | if (res.code === 200) { 19 | return res.responseData as CustomFileVulnerability; 20 | } 21 | return null; 22 | } 23 | 24 | const listofAllVulnerabilities = async (e: TextEditor) => { 25 | if (e === undefined || e === null) { 26 | return; 27 | } 28 | // const activeTexEditor = await window.showTextDocument(e.document); 29 | const activeTexEditor = e; 30 | if (activeTexEditor === undefined || activeTexEditor === null) { 31 | return; 32 | } 33 | 34 | const activeFeatureController = featureController.getSlot(); 35 | 36 | let fileName = await getFilePathuri(activeTexEditor.document.fileName); 37 | if (fileName === undefined || fileName === null) { 38 | closeStatusBarItem(); 39 | return; 40 | } 41 | switch (activeFeatureController) { 42 | case 'scan': 43 | { 44 | const vulnerabilities = await getFileFromCache(fileName); 45 | if (isNotNull(vulnerabilities)) { 46 | await openVulFile(vulnerabilities as CustomFileVulnerability, 'scan'); 47 | } else { 48 | closeStatusBarItem(); 49 | window.showWarningMessage( 50 | `${localeI18ln.getTranslation('persistResponse.vulnerabilityNotFound')}` 51 | ); 52 | } 53 | } 54 | break; 55 | case 'assess': 56 | { 57 | fileName = path.basename(fileName); 58 | const vulnerabilities = await getAssessVulnerabilitybyFile(fileName); 59 | if ( 60 | isNotNull(vulnerabilities) && 61 | isNotNull(vulnerabilities?.responseData) && 62 | vulnerabilities.code === 200 63 | ) { 64 | await openVulFile( 65 | vulnerabilities.responseData as CustomFileVulnerability, 66 | 'assess' 67 | ); 68 | } else { 69 | closeStatusBarItem(); 70 | window.showWarningMessage( 71 | `${localeI18ln.getTranslation('persistResponse.vulnerabilityNotFound')}` 72 | ); 73 | } 74 | } 75 | break; 76 | } 77 | // } 78 | }; 79 | 80 | export { listofAllVulnerabilities }; 81 | -------------------------------------------------------------------------------- /src/webview/screens/Assess/tabs/VulnerabilityReport/tabs/HowToFix.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | AssessVulnerability, 4 | VulnerabilityHowToFix, 5 | } from '../../../../../../common/types'; 6 | import { renderContent } from '../../../../../utils/formattedText'; 7 | 8 | export function HowToFix({ vulnerability }: { vulnerability: unknown }) { 9 | const [howToFix, setHowToFix] = useState(null); 10 | 11 | useEffect(() => { 12 | if (vulnerability !== null && vulnerability !== undefined) { 13 | const data = vulnerability as AssessVulnerability; 14 | setHowToFix(data.howToFix as VulnerabilityHowToFix); 15 | } 16 | }, [vulnerability]); 17 | 18 | return ( 19 |
20 |
26 |
{howToFix?.custom_recommendation?.text}
27 | {howToFix?.cwe !== null && 28 | howToFix?.cwe !== undefined && 29 | howToFix?.cwe !== '' && ( 30 |
31 |
CWE
32 |
33 | {howToFix.cwe} 34 |
35 |
36 | )} 37 | {howToFix?.owasp !== null && 38 | howToFix?.owasp !== undefined && 39 | howToFix?.owasp !== '' && ( 40 |
41 |
OWASP
42 |
43 | {howToFix.owasp} 44 |
45 |
46 | )} 47 | {howToFix?.rule_references?.text !== null && 48 | howToFix?.rule_references?.text !== undefined && 49 | howToFix?.rule_references?.text !== '' && ( 50 |
51 |
References
52 | 57 |
58 | )} 59 | 60 | {howToFix?.custom_rule_references?.text !== null && 61 | howToFix?.custom_rule_references?.text !== undefined && 62 | howToFix?.custom_rule_references?.text !== '' && ( 63 |
64 |
Custom References
65 | 70 |
71 | )} 72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/webview/screens/Assess/tabs/VulnerabilityReport/tabs/HttpRequest.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | AssessFileVulnerability, 4 | AssessVulnerability, 5 | ReducerTypes, 6 | VulnerabilityHttpRequest, 7 | } from '../../../../../../common/types'; 8 | import { useSelector } from 'react-redux'; 9 | import { 10 | AssessEventsHttpRequestUpdate, 11 | findVulnerabilityByTraceId, 12 | } from '../../../../../utils/helper'; 13 | export function HttpRequest({ vulnerability }: { vulnerability: unknown }) { 14 | const [httpRequest, setHttpRequest] = useState({ 15 | text: '', 16 | }); 17 | const vulnerabilitiesList = useSelector( 18 | (state: ReducerTypes) => state.assessFilter.allFiles 19 | ); 20 | const scaAutoRefresh = useSelector( 21 | (state: ReducerTypes) => state.assessFilter.scaAutoRefresh 22 | ); 23 | const [isHttpRequestthere, setHttpRequestIsTere] = useState(false); 24 | const [traceId, setTraceId] = useState(null); 25 | useEffect(() => { 26 | if (vulnerability !== null && vulnerability !== undefined) { 27 | const data = vulnerability as AssessVulnerability; 28 | setTraceId(data.traceId ?? null); 29 | if ( 30 | data.http_request !== null && 31 | data.http_request !== undefined && 32 | 'text' in data.http_request 33 | ) { 34 | if (data.http_request.text.length > 0) { 35 | setHttpRequestIsTere(true); 36 | } 37 | setHttpRequest({ text: data.http_request.text ?? '' }); 38 | } 39 | } 40 | }, [vulnerability]); 41 | 42 | useEffect(() => { 43 | if ( 44 | vulnerabilitiesList !== undefined && 45 | vulnerabilitiesList !== null && 46 | vulnerabilitiesList.responseData !== null && 47 | vulnerabilitiesList.responseData !== undefined 48 | ) { 49 | const vul = 50 | vulnerabilitiesList.responseData as unknown as AssessFileVulnerability & 51 | AssessVulnerability; 52 | if (isHttpRequestthere === false && traceId !== null) { 53 | const b = findVulnerabilityByTraceId([vul], traceId); 54 | if (b !== null && b !== undefined) { 55 | const events = b.http_request; 56 | 57 | if (events !== undefined && events !== null && 'text' in events) { 58 | setHttpRequest({ text: events?.text ?? '' }); 59 | } 60 | } 61 | } 62 | } 63 | }, [vulnerabilitiesList, isHttpRequestthere, traceId]); 64 | 65 | useEffect(() => { 66 | if ( 67 | !isHttpRequestthere && 68 | vulnerability !== null && 69 | vulnerability !== undefined && 70 | scaAutoRefresh !== null 71 | ) { 72 | const node = vulnerability as unknown as AssessVulnerability; 73 | AssessEventsHttpRequestUpdate(node); 74 | } 75 | }, [isHttpRequestthere, scaAutoRefresh]); 76 | 77 | return
{httpRequest.text}
; 78 | } 79 | -------------------------------------------------------------------------------- /src/webview/screens/Scan/tabs/AllVulnerability/AllVulnerabilityFiles.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { FC, useEffect } from 'react'; 3 | import { VulnerabilityReport } from '../../../../components/Scan/Vulnerability/VulnerabilityReport'; 4 | import { webviewPostMessage } from '../../../../utils/postMessage'; 5 | import { 6 | WEBVIEW_COMMANDS, 7 | WEBVIEW_SCREENS, 8 | } from '../../../../../vscode-extension/utils/constants/commands'; 9 | import { useSelector } from 'react-redux'; 10 | import { 11 | LocalizationJSON, 12 | ProjectVulnerability, 13 | ReducerTypes, 14 | } from '../../../../../common/types'; 15 | 16 | const AllVulnerabilityFiles: FC = () => { 17 | const getAllVulFromState = useSelector( 18 | (state: ReducerTypes) => state.vulnerability.allFiles 19 | ); 20 | 21 | const i18nData = useSelector((state: ReducerTypes) => state.i10ln.data); 22 | const [i18nFields, updateI18nFields] = useState( 23 | "

No vulnerabilities found for the project:

  1. Navigate to the Contrast - Scan menu
  2. Select the necessary filters and click Run Button.
  3. View the results in the Vulnerability Report tab.

After retrieving vulnerabilities, return to this screen or else click on refresh icon to see the latest vulnerability report.

Tip: You can access this screen anytime via the Contrast Scan panel in the Activity Bar.

" 24 | ); 25 | 26 | const [allfileVul, setAllFileVul] = useState([]); 27 | 28 | useEffect(() => { 29 | if (i18nData !== null && i18nData !== undefined) { 30 | const { vulnerabilityReport } = 31 | i18nData as LocalizationJSON['contrastScan']; 32 | 33 | updateI18nFields(vulnerabilityReport?.htmlElements.translate); 34 | } 35 | }, [i18nData]); 36 | 37 | useEffect(() => { 38 | webviewPostMessage({ 39 | command: WEBVIEW_COMMANDS.SCAN_GET_ALL_FILES_VULNERABILITY, 40 | payload: null, 41 | screen: WEBVIEW_SCREENS.SCAN, 42 | }); 43 | }, []); 44 | 45 | useEffect(() => { 46 | if ( 47 | getAllVulFromState !== null && 48 | getAllVulFromState !== undefined && 49 | getAllVulFromState?.responseData !== undefined && 50 | getAllVulFromState.responseData !== null && 51 | getAllVulFromState?.code === 200 52 | ) { 53 | setAllFileVul([getAllVulFromState.responseData as ProjectVulnerability]); 54 | } else { 55 | setAllFileVul([]); 56 | } 57 | }, [getAllVulFromState]); 58 | return ( 59 | <> 60 | <> 61 | {allfileVul.length === 0 || allfileVul === null ? ( 62 |
66 | ) : ( 67 | 68 | )} 69 | 70 | 71 | ); 72 | }; 73 | 74 | export { AllVulnerabilityFiles }; 75 | -------------------------------------------------------------------------------- /src/test/unitTest/webview/projectSlice.test.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | addConfigureProject, 3 | getAllConfiguredProjects, 4 | getAllProjectList, 5 | setCancelStateWhileDelete, 6 | updateConfiguredProjectDelete, 7 | updateConfigureProject, 8 | } from '../../../webview/utils/redux/slices/projectsSlice'; 9 | import { ProjectReducer } from '../../../webview/utils/redux/slices/projectsSlice'; 10 | 11 | describe('projectSlice', () => { 12 | const initialState = { 13 | configuredProjects: null, 14 | allProjectList: null, 15 | allApplicationList: null, 16 | configuredProjectDelete: null, 17 | addConfigureProject: null, 18 | updateConfigureProject: null, 19 | cancelStateWhileDelete: false, 20 | settingActions: false, 21 | /* eslint-disable @typescript-eslint/no-explicit-any */ 22 | } as any; 23 | 24 | it('should return the initial state when no action is passed', () => { 25 | expect( 26 | ProjectReducer(undefined, { 27 | type: '', 28 | }) 29 | ).toEqual(initialState); 30 | }); 31 | 32 | it('should handle getAllConfiguredProjects action', () => { 33 | const action = getAllConfiguredProjects([{ id: 1, name: 'Project 1' }]); 34 | const newState = ProjectReducer(initialState, action); 35 | expect(newState.configuredProjects).toEqual([{ id: 1, name: 'Project 1' }]); 36 | }); 37 | 38 | it('should handle getAllProjectList action', () => { 39 | const action = getAllProjectList([{ id: 1, name: 'Project 1' }]); 40 | const newState = ProjectReducer(initialState, action); 41 | expect(newState.allProjectList).toEqual([{ id: 1, name: 'Project 1' }]); 42 | }); 43 | 44 | it('should handle updateConfiguredProjectDelete action', () => { 45 | const action = updateConfiguredProjectDelete({ 46 | id: 1, 47 | name: 'Deleted Project', 48 | }); 49 | const newState = ProjectReducer(initialState, action); 50 | expect(newState.configuredProjectDelete).toEqual({ 51 | id: 1, 52 | name: 'Deleted Project', 53 | }); 54 | }); 55 | 56 | it('should handle addConfigureProject action', () => { 57 | const action = addConfigureProject({ id: 1, name: 'New Project' }); 58 | const newState = ProjectReducer(initialState, action); 59 | expect(newState.addConfigureProject).toEqual({ 60 | id: 1, 61 | name: 'New Project', 62 | }); 63 | }); 64 | 65 | it('should handle updateConfigureProject action', () => { 66 | const action = updateConfigureProject({ id: 1, name: 'Updated Project' }); 67 | const newState = ProjectReducer(initialState, action); 68 | expect(newState.updateConfigureProject).toEqual({ 69 | id: 1, 70 | name: 'Updated Project', 71 | }); 72 | }); 73 | it('should handle cancelStateWhileDelete action', () => { 74 | const action = setCancelStateWhileDelete(true); 75 | const newState = ProjectReducer(initialState, action); 76 | expect(newState.cancelStateWhileDelete).toEqual(true); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/vscode-extension/commands/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { registerContrastActivityBar } from './ui-commands/openActivityBar'; 3 | import { registerContrastPanel } from './ui-commands/webviewHandler'; 4 | import { registerSettingsCommand } from './ui-commands/settingsCommand'; 5 | import { registerScanCommand } from './ui-commands/scanCommands'; 6 | import { PersistenceInstance } from '../utils/persistanceState'; 7 | import { initializeLogger } from '../logging/logger'; 8 | import { registerRetrieveVulCommand } from './ui-commands/retrieveVulnerabilityCommand'; 9 | import { 10 | featureController, 11 | libraryPathNavigator, 12 | slotInstance, 13 | tabBlocker, 14 | } from '../utils/helper'; 15 | import { listofAllVulnerabilities } from '../utils/listofAllVulnerabilities'; 16 | import { registerStatusBarCommend } from '../utils/statusBarSeverity'; 17 | import { registerAboutWebviewPanel } from './ui-commands/aboutWebviewHandler'; 18 | import { SCAN_KEYS, TOKEN } from '../utils/constants/commands'; 19 | import { FilterData } from '../../webview/utils/constant'; 20 | import { PersistedDTO } from '../../common/types'; 21 | import { registerAssessCommand } from './ui-commands/assessCommand'; 22 | import { globalConfigChangeListener } from '../utils/multiInstanceConfigSync'; 23 | import { LocaleMemoryCacheInstance } from '../utils/localeMemoryCache'; 24 | import { SecretService } from '../utils/encryptDecrypt'; 25 | 26 | let globalExtentionUri: vscode.ExtensionContext; 27 | export let secrets: SecretService; 28 | 29 | const registeredCommands = [ 30 | registerSettingsCommand, 31 | registerScanCommand, 32 | registerContrastActivityBar, 33 | registerContrastPanel, 34 | registerRetrieveVulCommand, 35 | registerStatusBarCommend, 36 | registerAboutWebviewPanel, 37 | registerAssessCommand, 38 | globalConfigChangeListener, 39 | ]; 40 | 41 | export async function registerCommands( 42 | context: vscode.ExtensionContext 43 | ): Promise { 44 | secrets = new SecretService(context.secrets); 45 | globalExtentionUri = context; 46 | PersistenceInstance.registerContext(context.globalState); 47 | 48 | tabBlocker(); 49 | 50 | vscode.window.onDidChangeActiveTextEditor(async (e) => { 51 | if ( 52 | e && 53 | slotInstance.getSlot() === true && 54 | featureController.getSlot() !== 'none' && 55 | libraryPathNavigator.getSlot() === true 56 | ) { 57 | await listofAllVulnerabilities(e); 58 | } 59 | }); 60 | 61 | initializeLogger(context); 62 | context.subscriptions.push(...registeredCommands); 63 | 64 | await LocaleMemoryCacheInstance.setItem( 65 | TOKEN.SCAN, 66 | SCAN_KEYS.FILTERS as keyof PersistedDTO, 67 | FilterData 68 | ); 69 | await LocaleMemoryCacheInstance.clearStore(TOKEN.ASSESS); 70 | } 71 | 72 | export const disposeCommads = () => { 73 | registeredCommands.map((item) => { 74 | if ('dispose' in item) { 75 | item.dispose(); 76 | } 77 | }); 78 | }; 79 | export { globalExtentionUri }; 80 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | - This folder contains all of the files necessary for your extension. 6 | - `package.json` - this is the manifest file in which you declare your extension and command. 7 | - The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | - `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | - The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | - We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Setup 13 | 14 | - install the recommended extensions (amodio.tsl-problem-matcher, ms-vscode.extension-test-runner, and dbaeumer.vscode-eslint) 15 | 16 | ## Get up and running straight away 17 | 18 | - Press `F5` to open a new window with your extension loaded. 19 | - Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 20 | - Set breakpoints in your code inside `src/extension.ts` to debug your extension. 21 | - Find output from your extension in the debug console. 22 | 23 | ## Make changes 24 | 25 | - You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 26 | - You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 27 | 28 | ## Explore the API 29 | 30 | - You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 31 | 32 | ## Run tests 33 | 34 | - Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner) 35 | - Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered. 36 | - Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A` 37 | - See the output of the test result in the Test Results view. 38 | - Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder. 39 | - The provided test runner will only consider files matching the name pattern `**.test.ts`. 40 | - You can create folders inside the `test` folder to structure your tests any way you want. 41 | 42 | ## Go further 43 | 44 | - Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 45 | - [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 46 | - Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 47 | -------------------------------------------------------------------------------- /src/test/unitTest/webview/Overview.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { Overview } from '../../../webview/screens/Assess/tabs/VulnerabilityReport/tabs/Overview'; 4 | import { PassLocalLang } from '../../../common/types'; 5 | 6 | describe('Overview Component', () => { 7 | const mockVulnerability = { 8 | overview: { 9 | chapters: [ 10 | { 11 | introText: 'Intro to vulnerability', 12 | body: 'Detailed body of vulnerability', 13 | }, 14 | ], 15 | risk: { text: 'High risk' }, 16 | }, 17 | popupMessage: { 18 | lastDetected_date: '2024-03-01, 12:00 PM', 19 | firstDetected_date: '2024-01-01, 08:00 AM', 20 | }, 21 | }; 22 | 23 | const mockTranslate = { 24 | vulnerabilityReport: { 25 | tabs: { 26 | overView: { 27 | formFields: { 28 | whatHappened: { translate: 'What happened?' }, 29 | whatsTheRisk: { translate: "What's the risk?" }, 30 | firstDetectedDate: { translate: 'First Detected Date' }, 31 | lastDetectedDate: { translate: 'Last Detected Date' }, 32 | }, 33 | }, 34 | }, 35 | }, 36 | }; 37 | 38 | test('renders the Overview component correctly', () => { 39 | render( 40 | 44 | ); 45 | expect(screen.getByText('What happened?')).toBeInTheDocument(); 46 | expect(screen.getByText('Intro to vulnerability')).toBeInTheDocument(); 47 | expect( 48 | screen.getByText('Detailed body of vulnerability') 49 | ).toBeInTheDocument(); 50 | expect(screen.getByText("What's the risk?")).toBeInTheDocument(); 51 | expect(screen.getByText('High risk')).toBeInTheDocument(); 52 | expect(screen.getByText('2024-01-01')).toBeInTheDocument(); 53 | expect(screen.getByText('2024-03-01')).toBeInTheDocument(); 54 | }); 55 | 56 | test('handles missing chapters correctly', () => { 57 | const modifiedVulnerability = { 58 | ...mockVulnerability, 59 | overview: { risk: { text: 'Medium risk' } }, 60 | }; 61 | render( 62 | 66 | ); 67 | expect(screen.getByText('Medium risk')).toBeInTheDocument(); 68 | }); 69 | 70 | test('handles missing risk correctly', () => { 71 | const modifiedVulnerability = { 72 | ...mockVulnerability, 73 | overview: { chapters: [] }, 74 | }; 75 | render( 76 | 80 | ); 81 | expect(screen.queryByText('High risk')).not.toBeInTheDocument(); 82 | }); 83 | 84 | test('renders with missing translation correctly', async () => { 85 | const modifiedTranslate = { 86 | vulnerabilityReport: { tabs: { overView: {} } }, 87 | }; 88 | render( 89 | 93 | ); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/test/unitTest/webview/HowToFix.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { HowToFix } from '../../../webview/screens/Assess/tabs/VulnerabilityReport/tabs/HowToFix'; 4 | // import { HowToFix } from './HowToFix'; 5 | 6 | describe('HowToFix Component', () => { 7 | const vulnerabilityMock = { 8 | howToFix: { 9 | recommendation: { formattedText: 'Fix this issue' }, 10 | custom_recommendation: { text: 'Custom fix' }, 11 | cwe: 'https://cwe.mitre.org/data/definitions/79.html', 12 | owasp: 'https://owasp.org/www-project-top-ten/', 13 | rule_references: { text: 'https://security-rules.com/rule' }, 14 | custom_rule_references: { text: 'https://custom-rules.com/rule' }, 15 | }, 16 | }; 17 | 18 | test('renders recommendation text', () => { 19 | render(); 20 | expect(screen.getByText('Fix this issue')).toBeInTheDocument(); 21 | expect(screen.getByText('Custom fix')).toBeInTheDocument(); 22 | }); 23 | 24 | test('renders CWE link when present', () => { 25 | render(); 26 | const cweLink = screen.getByText( 27 | 'https://cwe.mitre.org/data/definitions/79.html' 28 | ); 29 | expect(cweLink).toBeInTheDocument(); 30 | expect(cweLink.closest('a')).toHaveAttribute( 31 | 'href', 32 | vulnerabilityMock.howToFix.cwe 33 | ); 34 | }); 35 | 36 | test('renders OWASP link when present', () => { 37 | render(); 38 | const owaspLink = screen.getByText( 39 | 'https://owasp.org/www-project-top-ten/' 40 | ); 41 | expect(owaspLink).toBeInTheDocument(); 42 | expect(owaspLink.closest('a')).toHaveAttribute( 43 | 'href', 44 | vulnerabilityMock.howToFix.owasp 45 | ); 46 | }); 47 | 48 | test('renders rule references link when present', () => { 49 | render(); 50 | const ruleRefLink = screen.getByText('https://security-rules.com/rule'); 51 | expect(ruleRefLink).toBeInTheDocument(); 52 | expect(ruleRefLink.closest('a')).toHaveAttribute( 53 | 'href', 54 | vulnerabilityMock.howToFix.rule_references.text 55 | ); 56 | }); 57 | 58 | test('renders custom rule references link when present', () => { 59 | render(); 60 | const customRuleRefLink = screen.getByText('https://custom-rules.com/rule'); 61 | expect(customRuleRefLink).toBeInTheDocument(); 62 | expect(customRuleRefLink.closest('a')).toHaveAttribute( 63 | 'href', 64 | vulnerabilityMock.howToFix.custom_rule_references.text 65 | ); 66 | }); 67 | 68 | test('does not render CWE section when CWE is missing', () => { 69 | const modifiedMock = { 70 | howToFix: { ...vulnerabilityMock.howToFix, cwe: '' }, 71 | }; 72 | render(); 73 | expect(screen.queryByText('CWE')).not.toBeInTheDocument(); 74 | }); 75 | 76 | test('does not render OWASP section when OWASP is missing', () => { 77 | const modifiedMock = { 78 | howToFix: { ...vulnerabilityMock.howToFix, owasp: '' }, 79 | }; 80 | render(); 81 | expect(screen.queryByText('OWASP')).not.toBeInTheDocument(); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/test/unitTest/vscode-extension/ContrastTreeItem.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { Uri } from 'vscode'; 3 | import { 4 | ContrastNavigationItems, 5 | ContrastTreeItem, 6 | } from '../../../vscode-extension/utils/treeItems'; 7 | import { 8 | CONSTRAST_ABOUT, 9 | CONSTRAST_SCAN, 10 | CONSTRAST_SETTING, 11 | } from '../../../vscode-extension/utils/constants/commands'; 12 | 13 | jest.mock('vscode', () => ({ 14 | env: { 15 | language: 'en', 16 | appName: 'VSCode', 17 | }, 18 | workspace: { 19 | workspaceFolders: [{ uri: { fsPath: '/path/to/mock/workspace' } }], 20 | }, 21 | TreeItem: class { 22 | [x: string]: { dark: Uri; light: Uri }; 23 | constructor( 24 | label: { dark: Uri; light: Uri }, 25 | /* eslint-disable @typescript-eslint/no-explicit-any */ 26 | command: any = null, 27 | /* eslint-disable @typescript-eslint/no-explicit-any */ 28 | icon: any = null 29 | ) { 30 | this.label = label; 31 | if (command !== null) { 32 | this.command = { 33 | title: label, 34 | command: command, 35 | } as any; 36 | } 37 | if (icon !== null) { 38 | const projectRoot = path.resolve(__dirname, '..'); 39 | const iconPath = Uri.file(path.join(projectRoot, 'assets', icon)); 40 | this.iconPath = { 41 | dark: iconPath, 42 | light: iconPath, 43 | }; 44 | } 45 | } 46 | }, 47 | Uri: { 48 | file: jest.fn().mockReturnValue('mockUri'), 49 | }, 50 | commands: { 51 | registerCommand: jest.fn(), 52 | }, 53 | languages: { 54 | registerHoverProvider: jest.fn(), 55 | }, 56 | })); 57 | 58 | describe('ContrastTreeItem', () => { 59 | it('should create an instance of ContrastTreeItem', () => { 60 | const treeItem = new ContrastTreeItem( 61 | 'Test Label', 62 | 'test.command', 63 | 'test-icon.png' 64 | ); 65 | 66 | expect(treeItem.label).toBe('Test Label'); 67 | expect(treeItem.command).toEqual({ 68 | title: 'Test Label', 69 | command: 'test.command', 70 | }); 71 | expect(treeItem.iconPath).toEqual({ 72 | dark: 'mockUri', 73 | light: 'mockUri', 74 | }); 75 | }); 76 | 77 | it('should create an instance of ContrastTreeItem without command and icon', () => { 78 | const treeItem = new ContrastTreeItem('Test Label'); 79 | 80 | expect(treeItem.label).toBe('Test Label'); 81 | expect(treeItem.command).not.toBe(null); 82 | expect(treeItem.iconPath).toBeUndefined(); 83 | }); 84 | 85 | describe('ContrastNavigationItems', () => { 86 | it('should contain the correct number of ContrastTreeItems', () => { 87 | expect(ContrastNavigationItems.length).toBe(4); 88 | }); 89 | 90 | it('should have the correct labels and commands', () => { 91 | expect(ContrastNavigationItems[0].label).toBe('About'); 92 | expect(ContrastNavigationItems[0].command).toEqual({ 93 | title: 'About', 94 | command: CONSTRAST_ABOUT, 95 | }); 96 | 97 | expect(ContrastNavigationItems[1].label).toBe('Contrast - Settings'); 98 | expect(ContrastNavigationItems[1].command).toEqual({ 99 | title: 'Contrast - Settings', 100 | command: CONSTRAST_SETTING, 101 | }); 102 | 103 | expect(ContrastNavigationItems[2].label).toBe('Contrast - Scan'); 104 | expect(ContrastNavigationItems[2].command).toEqual({ 105 | title: 'Contrast - Scan', 106 | command: CONSTRAST_SCAN, 107 | }); 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/vscode-extension/cache/backgroundRefreshTimer.ts: -------------------------------------------------------------------------------- 1 | import { localeI18ln } from '../../l10n'; 2 | import { DateTime, getVulnerabilitiesRefreshCycle } from '../utils/commonUtil'; 3 | import { 4 | SETTING_KEYS, 5 | TOKEN, 6 | WEBVIEW_COMMANDS, 7 | } from '../utils/constants/commands'; 8 | import { PersistenceInstance } from '../utils/persistanceState'; 9 | import { 10 | clearCacheByProjectId, 11 | getDataOnlyFromCache, 12 | refreshCache, 13 | } from './cacheManager'; 14 | import { ShowInformationPopup } from '../commands/ui-commands/messageHandler'; 15 | import { window } from 'vscode'; 16 | import { ContrastPanelInstance } from '../commands/ui-commands/webviewHandler'; 17 | import { ConfiguredProject, LogLevel, PersistedDTO } from '../../common/types'; 18 | import { loggerInstance } from '../logging/logger'; 19 | 20 | //to set inteval based on the recycle time 21 | let interval: ReturnType | undefined; 22 | export async function startBackgroundTimer(projectId: string): Promise { 23 | const startTime: string = DateTime(); 24 | const persistedData = PersistenceInstance.getByKey( 25 | TOKEN.SETTING, 26 | SETTING_KEYS.CONFIGPROJECT as keyof PersistedDTO 27 | ) as ConfiguredProject[]; 28 | const project = persistedData.find( 29 | (project: ConfiguredProject) => project?.projectId === projectId 30 | ); 31 | if (project === null) { 32 | throw new Error('Project not found'); 33 | } 34 | const formatDate = (date: Date) => { 35 | /* eslint-disable @typescript-eslint/no-explicit-any */ 36 | const pad = (num: any) => String(num).padStart(2, '0'); 37 | return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; 38 | }; 39 | const refreshCycle = await getVulnerabilitiesRefreshCycle( 40 | project?.projectId as string 41 | ); 42 | interval = setInterval( 43 | async () => { 44 | try { 45 | const project = persistedData.find( 46 | (project: ConfiguredProject) => project?.projectId === projectId 47 | ); 48 | if (project !== null && project !== undefined) { 49 | ShowInformationPopup( 50 | localeI18ln.getTranslation('apiResponse.cacheStarted') 51 | ); 52 | } 53 | const result = await refreshCache(projectId); 54 | if (result !== undefined) { 55 | ContrastPanelInstance.postMessage({ 56 | command: WEBVIEW_COMMANDS.SCAN_GET_ALL_FILES_VULNERABILITY, 57 | data: await getDataOnlyFromCache(), 58 | }); 59 | window.showInformationMessage( 60 | `${localeI18ln.getTranslation('persistResponse.autoRefreshSucess')} ${formatDate(new Date())}` 61 | ); 62 | const logData = `Start Time: ${startTime} | End Time: ${DateTime()} | Message: Auto-Refresh - Vulnerability Sync Process Completed`; 63 | void loggerInstance?.logMessage(LogLevel.INFO, logData); 64 | } 65 | } catch (err) { 66 | if (err instanceof Error) { 67 | const logData = `Start Time: ${startTime} | End Time: ${DateTime()} | Message: Auto-Refresh - ${err.message} \n`; 68 | void loggerInstance?.logMessage(LogLevel.ERROR, logData); 69 | } 70 | console.error( 71 | localeI18ln.getTranslation('apiResponse.failedToRefreshCache'), 72 | err, 73 | ShowInformationPopup( 74 | localeI18ln.getTranslation('apiResponse.failedToRefreshCache') 75 | ) 76 | ); 77 | } 78 | }, 79 | refreshCycle * 60 * 1000 80 | ); 81 | } 82 | 83 | export async function stopBackgroundTimer(): Promise { 84 | if (interval !== undefined) { 85 | clearInterval(interval); 86 | } 87 | } 88 | 89 | export async function resetBackgroundTimer(projectId: string): Promise { 90 | if (interval !== undefined) { 91 | clearInterval(interval); 92 | } 93 | 94 | await clearCacheByProjectId(projectId); 95 | 96 | await startBackgroundTimer(projectId); 97 | } 98 | -------------------------------------------------------------------------------- /src/webview/screens/Assess/tabs/VulnerabilityReport/tabs/Overview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | AssessVulnerability, 4 | ContrastAssessLocale, 5 | PassLocalLang, 6 | VulnerabilityOverview, 7 | } from '../../../../../../common/types'; 8 | import { renderContent } from '../../../../../utils/formattedText'; 9 | 10 | interface PropertiesValues { 11 | whatHappened: string; 12 | whatsTheRisk: string; 13 | firstDetectedDate: string; 14 | lastDetectedDate: string; 15 | } 16 | export function Overview({ 17 | translate, 18 | vulnerability, 19 | }: { 20 | translate: PassLocalLang; 21 | vulnerability: unknown; 22 | }) { 23 | const [overviewData, setOverView] = useState(); 24 | useEffect(() => { 25 | if (vulnerability !== null && vulnerability !== undefined) { 26 | const data = vulnerability as AssessVulnerability; 27 | let obj: VulnerabilityOverview = { ...data.overview }; 28 | if ( 29 | obj.chapters !== undefined && 30 | obj.chapters !== null && 31 | !Array.isArray(obj.chapters) 32 | ) { 33 | obj = { 34 | ...obj, 35 | chapters: [obj.chapters], 36 | }; 37 | } 38 | setOverView(obj); 39 | const splitData = (dateTime: string) => 40 | dateTime.slice(0, dateTime.indexOf(',')) || ''; 41 | setDate({ 42 | lastDate: splitData(data.popupMessage.lastDetected_date as string), 43 | firstDate: splitData(data.popupMessage.firstDetected_date as string), 44 | }); 45 | } 46 | }, [vulnerability]); 47 | 48 | const [properties, setProperties] = useState({ 49 | whatHappened: 'What happened?', 50 | whatsTheRisk: "What's the risk?", 51 | firstDetectedDate: 'First Detected Date', 52 | lastDetectedDate: 'Last Detected Date', 53 | }); 54 | 55 | const [date, setDate] = useState({ firstDate: '', lastDate: '' }); 56 | 57 | useEffect(() => { 58 | if (translate !== null && translate !== undefined) { 59 | const response = translate as unknown as ContrastAssessLocale; 60 | const overView = response.vulnerabilityReport?.tabs?.overView; 61 | setProperties({ 62 | whatHappened: overView?.formFields?.whatHappened.translate as string, 63 | whatsTheRisk: overView?.formFields?.whatsTheRisk.translate as string, 64 | firstDetectedDate: overView?.formFields?.firstDetectedDate 65 | .translate as string, 66 | lastDetectedDate: overView?.formFields?.lastDetectedDate 67 | .translate as string, 68 | }); 69 | } 70 | }, [translate]); 71 | 72 | return ( 73 |
74 |
75 |
{properties.whatHappened}
76 |
77 | {overviewData?.chapters?.map((item, index) => { 78 | return ( 79 |
80 |
{item.introText}
81 | {item.body &&
{item.body}
} 82 |
83 | ); 84 | })} 85 |
86 |
87 |
88 |
{properties.whatsTheRisk}
89 |
95 |
96 |
97 | 98 | {properties.firstDetectedDate} : 99 | 100 | {' ' + date.firstDate} 101 |
102 |
103 | 104 | {properties.lastDetectedDate} : 105 | 106 | {' ' + date.lastDate} 107 |
108 |
109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /src/webview/screens/Assess/tabs/LibraryReport/tabs/LibraryHowToFix.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | ContrastAssessLocale, 4 | LibraryReportHowToFix, 5 | PassLocalLang, 6 | } from '../../../../../../common/types'; 7 | import { LibraryNode } from '../../../../../../vscode-extension/api/model/api.interface'; 8 | import { 9 | countOwnEntries, 10 | getGradeColorKey, 11 | isOfType, 12 | } from '../../../../../utils/helper'; 13 | 14 | export function LibraryHowToFix({ 15 | translate, 16 | vulnerability, 17 | }: { 18 | translate: PassLocalLang; 19 | vulnerability: unknown; 20 | }) { 21 | const [howToFixData, setHowToFix] = useState | null>( 22 | null 23 | ); 24 | useEffect(() => { 25 | if (vulnerability !== null && vulnerability !== undefined) { 26 | const response = vulnerability as unknown as LibraryNode; 27 | if (!isOfType(response, 'howToFix')) { 28 | return; 29 | } 30 | if ( 31 | countOwnEntries(response.howToFix.minUpgrade) > 0 || 32 | countOwnEntries(response.howToFix.maxUpgrade) > 0 33 | ) { 34 | setHowToFix(response); 35 | } else { 36 | setHowToFix(null); 37 | } 38 | } else { 39 | setHowToFix(null); 40 | } 41 | }, [vulnerability]); 42 | 43 | const [properties, setProperties] = useState({ 44 | translate: 'How To Fix', 45 | placeholder: 'No recommended fixes.', 46 | minimumUpgrade: { 47 | translate: 'Minimum upgrade', 48 | placeholder: 'We recommend upgrading to', 49 | }, 50 | latestStable: { 51 | translate: 'Latest stable', 52 | placeholder: 'We recommend upgrading to', 53 | }, 54 | }); 55 | 56 | useEffect(() => { 57 | if (translate !== null && translate !== undefined) { 58 | const response = translate as unknown as ContrastAssessLocale; 59 | const howToFix = response.librariesReport?.tabs 60 | ?.howToFix as LibraryReportHowToFix; 61 | setProperties(howToFix); 62 | } 63 | }, [translate]); 64 | 65 | return ( 66 | <> 67 | {howToFixData !== null ? ( 68 |
69 |
{properties.translate}
70 |
71 |
{properties.minimumUpgrade?.translate}
72 |
73 | {properties.minimumUpgrade?.placeholder} 74 |
75 |
83 | {howToFixData?.howToFix?.minUpgrade.grade} 84 |
85 |
86 | {howToFixData?.howToFix?.minUpgrade.version} 87 |
88 |
89 |
90 |
{properties.latestStable?.translate}
91 |
92 | {properties.latestStable?.placeholder} 93 |
94 |
102 | {howToFixData?.howToFix?.maxUpgrade.grade} 103 |
104 |
105 | {howToFixData?.howToFix?.maxUpgrade.version} 106 |
107 |
108 |
109 | ) : ( 110 | {properties.placeholder} 111 | )} 112 | 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /src/webview/utils/formattedText.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import Mustache from 'mustache'; 3 | import hljs from 'highlight.js'; 4 | import 'highlight.js/styles/default.css'; 5 | 6 | function buildSyntaxHighlighter(language: string) { 7 | return function () { 8 | return function (text: string, render: (text: string) => string) { 9 | const renderedText = render(text); 10 | if ( 11 | [ 12 | 'html', 13 | 'xml', 14 | 'csharp', 15 | 'java', 16 | 'javascript', 17 | 'ruby', 18 | 'python', 19 | 'php', 20 | 'go', 21 | ].includes(language) 22 | ) { 23 | return ( 24 | '
' +
 25 |           hljs.highlight(renderedText, { language }).value +
 26 |           '
' 27 | ); 28 | } 29 | return hljs.highlightAuto(renderedText).value; 30 | }; 31 | }; 32 | } 33 | 34 | export const customMustacheTags = { 35 | nl: '
', 36 | 37 | codeString() { 38 | return (text: string, render: (text: string) => string) => 39 | `${render(text)}`; 40 | }, 41 | 42 | block() { 43 | return (text: string, render: any) => `
${render(text)}
`; 44 | }, 45 | 46 | blockQuote() { 47 | return (text: string, render: any) => 48 | `
${render(text)}
`; 49 | }, 50 | 51 | code() { 52 | return (text: string, render: any) => `${render(text)}`; 53 | }, 54 | 55 | emphasize() { 56 | return (text: string, render: any) => `${render(text)}`; 57 | }, 58 | 59 | exampleText() { 60 | return (text: string, render: any) => `${render(text)}`; 61 | }, 62 | 63 | header() { 64 | return (text: string, render: any) => `

${render(text)}

`; 65 | }, 66 | 67 | link() { 68 | return (text: string, render: any) => { 69 | const [url, linkText] = render(text).split('$$LINK_DELIM$$'); 70 | return `${linkText}`; 71 | }; 72 | }, 73 | 74 | paragraph() { 75 | return (text: string, render: any) => `

${render(text)}

`; 76 | }, 77 | 78 | orderedList() { 79 | return (text: string, render: any) => `
    ${render(text)}
`; 80 | }, 81 | 82 | unorderedList() { 83 | return (text: string, render: any) => `
    ${render(text)}
`; 84 | }, 85 | 86 | listElement() { 87 | return (text: string, render: any) => `
  • ${render(text)}
  • `; 88 | }, 89 | 90 | table() { 91 | return (text: string, render: any) => 92 | `${render(text)}
    `; 93 | }, 94 | 95 | tableRow() { 96 | return (text: string, render: any) => `${render(text)}`; 97 | }, 98 | 99 | tableCell() { 100 | return (text: string, render: any) => `${render(text)}`; 101 | }, 102 | 103 | redacted() { 104 | return (text: string) => 105 | `${text}`; 106 | }, 107 | 108 | badConfig() { 109 | return (text: string, render: any) => 110 | `${render(text)}`; 111 | }, 112 | 113 | goodConfig() { 114 | return (text: string, render: any) => 115 | `${render(text)}`; 116 | }, 117 | 118 | // Syntax highlight code blocks 119 | csharpBlock: buildSyntaxHighlighter('csharp'), 120 | javaBlock: buildSyntaxHighlighter('java'), 121 | javascriptBlock: buildSyntaxHighlighter('javascript'), 122 | rubyBlock: buildSyntaxHighlighter('ruby'), 123 | pythonBlock: buildSyntaxHighlighter('python'), 124 | phpBlock: buildSyntaxHighlighter('php'), 125 | goBlock: buildSyntaxHighlighter('go'), 126 | htmlBlock: buildSyntaxHighlighter('html'), 127 | xmlBlock: buildSyntaxHighlighter('xml'), 128 | }; 129 | 130 | export function renderContent(content: string) { 131 | return Mustache.render(content, customMustacheTags); 132 | } 133 | -------------------------------------------------------------------------------- /src/test/unitTest/vscode-extension/getPackageInformation.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { ApiResponse } from '../../../common/types'; 3 | import { getPackageInformation } from '../../../vscode-extension/api/services/apiService'; 4 | import path from 'path'; 5 | import { Uri } from 'vscode'; 6 | 7 | jest.mock('axios'); 8 | jest.mock('axios-retry'); 9 | jest.mock('../../../vscode-extension/utils/errorHandling', () => ({ 10 | resolveSuccess: jest.fn().mockReturnValue({ 11 | status: 'success', 12 | code: 200, 13 | message: 'Package info retrieved successfully', 14 | responseData: { 15 | name: 'test-package', 16 | version: '0.0.1', 17 | displayName: 'Test Package', 18 | description: 'A test package for testing', 19 | aboutPage: { 20 | title: 'About Test Package', 21 | content: 'This package is used for testing purposes.', 22 | }, 23 | osWithVersion: 'linux' + '6.0', 24 | IDEVersion: '1.93.0', 25 | platform: 'VSCode', 26 | }, 27 | }), 28 | })); 29 | 30 | jest.mock('vscode', () => { 31 | const UIKind = { Desktop: 1, Web: 2 }; 32 | 33 | return { 34 | UIKind, 35 | version: '1.93.0', 36 | env: { 37 | language: 'en', 38 | appName: 'VSCode', 39 | uiKind: UIKind.Desktop, 40 | }, 41 | workspace: { 42 | workspaceFolders: [{ uri: { fsPath: '/path/to/mock/workspace' } }], 43 | onDidChangeConfiguration: jest.fn(), 44 | }, 45 | window: { 46 | createTreeView: jest.fn().mockReturnValue({ 47 | onDidChangeVisibility: jest.fn(), 48 | }), 49 | activeTextEditor: { 50 | document: { 51 | fileName: 'test.js', 52 | }, 53 | }, 54 | }, 55 | TreeItem: class { 56 | [x: string]: { dark: Uri; light: Uri }; 57 | constructor( 58 | label: { dark: Uri; light: Uri }, 59 | command: any = null, 60 | icon: any = null 61 | ) { 62 | this.label = label; 63 | if (command !== null) { 64 | this.command = { title: label, command } as any; 65 | } 66 | if (icon !== null) { 67 | const projectRoot = path.resolve(__dirname, '..'); 68 | const iconPath = Uri.file(path.join(projectRoot, 'assets', icon)); 69 | this.iconPath = { dark: iconPath, light: iconPath }; 70 | } 71 | } 72 | }, 73 | Uri: { 74 | file: jest.fn().mockReturnValue('mockUri'), 75 | }, 76 | commands: { 77 | registerCommand: jest.fn(), 78 | }, 79 | languages: { 80 | registerHoverProvider: jest.fn(), 81 | }, 82 | }; 83 | }); 84 | 85 | jest.mock( 86 | '../../../vscode-extension/commands/ui-commands/webviewHandler', 87 | () => ({ 88 | ContrastPanelInstance: { 89 | postMessage: jest.fn(), 90 | }, 91 | }) 92 | ); 93 | 94 | const mockPkg = { 95 | name: 'test-package', 96 | version: '0.0.1', 97 | displayName: 'Test Package', 98 | description: 'A test package for testing', 99 | aboutPage: { 100 | title: 'About Test Package', 101 | content: 'This package is used for testing purposes.', 102 | }, 103 | osWithVersion: 'linux' + '6.0', 104 | IDEVersion: '1.93.0', 105 | platform: 'VSCode', 106 | }; 107 | 108 | describe('getPackageInformation', () => { 109 | afterAll(() => { 110 | jest.restoreAllMocks(); 111 | }); 112 | 113 | it('should return package information successfully', async () => { 114 | const result: ApiResponse = await getPackageInformation(); 115 | 116 | expect(result.status).toBe('success'); 117 | expect(result.code).toBe(200); 118 | expect(result.message).toBe('Package info retrieved successfully'); 119 | expect(result.responseData).toEqual({ 120 | name: mockPkg.name, 121 | displayName: mockPkg.displayName, 122 | version: mockPkg.version, 123 | description: mockPkg.description, 124 | aboutPage: mockPkg.aboutPage, 125 | osWithVersion: mockPkg.osWithVersion, 126 | IDEVersion: mockPkg.IDEVersion, 127 | platform: mockPkg.platform, 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /src/webview/hooks/useDateTime.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { getCurrentHour, isToday, isEqual, isLessThan } from '../utils/helper'; 3 | import { DateTimeValue, TimeSlotOption } from '../../common/types'; 4 | import { availableTimeSlots } from '../utils/constant'; 5 | 6 | interface DateTimeProps { 7 | fromDateTime: DateTimeValue; 8 | toDateTime: DateTimeValue; 9 | updateFromDateTime: (data: DateTimeValue) => void; 10 | updateToDateTime: (data: DateTimeValue) => void; 11 | setToTimeSlot: (data: TimeSlotOption[]) => void; 12 | setFromTimeSlot: (data: TimeSlotOption[]) => void; 13 | } 14 | 15 | export const useDateTime = ({ 16 | fromDateTime, 17 | toDateTime, 18 | updateToDateTime, 19 | setToTimeSlot, 20 | }: DateTimeProps) => { 21 | const [fromTimeSlotOptions, updateFromTimeSlotOptions] = useState< 22 | TimeSlotOption[] 23 | >([]); 24 | const [toTimeSlotOptions, updateToTimeSlotOptions] = useState< 25 | TimeSlotOption[] 26 | >([]); 27 | 28 | // Helper function to filter time slots within a range 29 | const filterTimeSlotOptionsByRange = (min: number, max: number) => 30 | availableTimeSlots.filter( 31 | (item) => +item.label >= min && +item.label <= max 32 | ); 33 | 34 | useEffect(() => { 35 | if (fromDateTime.date === null || toDateTime.date === null) { 36 | return; 37 | } 38 | 39 | if (isEqual(fromDateTime.date, toDateTime.date)) { 40 | isToday(fromDateTime.date) 41 | ? updateFromTimeSlotOptions( 42 | filterTimeSlotOptionsByRange(0, getCurrentHour()) 43 | ) 44 | : updateFromTimeSlotOptions(filterTimeSlotOptionsByRange(0, 23)); 45 | } else if (isLessThan(fromDateTime.date, toDateTime.date)) { 46 | isToday(toDateTime.date) && !isToday(fromDateTime.date) 47 | ? updateFromTimeSlotOptions(filterTimeSlotOptionsByRange(0, 23)) 48 | : updateFromTimeSlotOptions(filterTimeSlotOptionsByRange(0, 23)); 49 | } else { 50 | updateFromTimeSlotOptions(filterTimeSlotOptionsByRange(0, 23)); 51 | } 52 | }, [fromDateTime.date]); 53 | 54 | useEffect(() => { 55 | if (fromDateTime.date === null || toDateTime.date === null) { 56 | return; 57 | } 58 | setToTimeSlot([]); 59 | 60 | if (isEqual(fromDateTime.date, toDateTime.date)) { 61 | if (isToday(toDateTime.date)) { 62 | if (fromDateTime.time !== null) { 63 | updateToTimeSlotOptions( 64 | filterTimeSlotOptionsByRange( 65 | parseInt(fromDateTime.time), 66 | getCurrentHour() 67 | ) 68 | ); 69 | } 70 | } else { 71 | if (fromDateTime.time !== null) { 72 | updateToTimeSlotOptions( 73 | filterTimeSlotOptionsByRange(+fromDateTime.time, 23) 74 | ); 75 | } 76 | } 77 | } else if (isLessThan(fromDateTime.date, toDateTime.date)) { 78 | isToday(toDateTime.date) 79 | ? updateToTimeSlotOptions( 80 | filterTimeSlotOptionsByRange(0, getCurrentHour()) 81 | ) 82 | : updateToTimeSlotOptions(filterTimeSlotOptionsByRange(0, 23)); 83 | } 84 | }, [toDateTime.date, fromDateTime.time]); 85 | 86 | useEffect(() => { 87 | if (fromDateTime.date === null || toDateTime.date === null) { 88 | return; 89 | } 90 | 91 | if (isToday(fromDateTime.date) && !isToday(toDateTime.date)) { 92 | updateToDateTime({ ...toDateTime, date: fromDateTime.date }); 93 | updateFromTimeSlotOptions( 94 | filterTimeSlotOptionsByRange(0, getCurrentHour()) 95 | ); 96 | updateToTimeSlotOptions( 97 | filterTimeSlotOptionsByRange(0, getCurrentHour()) 98 | ); 99 | } else if (fromDateTime.date > toDateTime.date) { 100 | updateToDateTime({ ...toDateTime, date: fromDateTime.date }); 101 | updateToTimeSlotOptions( 102 | filterTimeSlotOptionsByRange( 103 | fromDateTime.time !== null ? +fromDateTime.time : 0, 104 | Infinity 105 | ) 106 | ); 107 | } 108 | }, [fromDateTime.date]); 109 | 110 | return { 111 | fromTimeSlotOptions, 112 | toTimeSlotOptions, 113 | }; 114 | }; 115 | -------------------------------------------------------------------------------- /src/test/unitTest/webview/ScanFilter.test.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { ScanState } from '../../../common/types'; 3 | import { 4 | getFilters, 5 | ScanReducer, 6 | setActiveCurrentFile, 7 | setVulnerabilityReport, 8 | } from '../../../webview/utils/redux/slices/ScanFilter'; 9 | 10 | import { VulReport } from '../../../common/types'; 11 | import { 12 | getAllFilesVulnerability, 13 | getCurrentFileVulnerability, 14 | vulnerabilityReducer, 15 | } from '../../../webview/utils/redux/slices/vulReport'; 16 | 17 | describe('scanSlice', () => { 18 | const initialState: ScanState = { 19 | filters: null, 20 | activeCurrentFile: null, 21 | activeVulnerabilityReport: null, 22 | scanRetrievelDetectAcrossIds: false, 23 | activeProjectName: null, 24 | backgroundVulnRunner: false, 25 | manualRefreshBackgroundVulnRunner: false, 26 | validWorkspaceProjects: [], 27 | }; 28 | 29 | const store = configureStore({ 30 | reducer: { 31 | scan: ScanReducer, 32 | }, 33 | }); 34 | 35 | it('should return the initial state', () => { 36 | const state = store.getState().scan; 37 | expect(state).toEqual(initialState); 38 | }); 39 | 40 | it('should handle getFilters action', () => { 41 | const filtersPayload = { someKey: 'someValue' }; 42 | 43 | store.dispatch(getFilters(filtersPayload)); 44 | 45 | const state = store.getState().scan; 46 | expect(state.filters).toEqual(filtersPayload); 47 | }); 48 | 49 | it('should handle setActiveCurrentFile action', () => { 50 | const activeCurrentFilePayload = true; 51 | 52 | store.dispatch(setActiveCurrentFile(activeCurrentFilePayload)); 53 | 54 | const state = store.getState().scan; 55 | expect(state.activeCurrentFile).toBe(activeCurrentFilePayload); 56 | }); 57 | 58 | it('should handle setVulnerabilityReport action', () => { 59 | const activeVulnerabilityReportPayload = true; 60 | 61 | store.dispatch(setVulnerabilityReport(activeVulnerabilityReportPayload)); 62 | 63 | const state = store.getState().scan; 64 | expect(state.activeVulnerabilityReport).toBe( 65 | activeVulnerabilityReportPayload 66 | ); 67 | }); 68 | 69 | it('should handle multiple actions correctly', () => { 70 | const filtersPayload = { someKey: 'someValue' }; 71 | const activeCurrentFilePayload = true; 72 | const activeVulnerabilityReportPayload = true; 73 | 74 | store.dispatch(getFilters(filtersPayload)); 75 | store.dispatch(setActiveCurrentFile(activeCurrentFilePayload)); 76 | store.dispatch(setVulnerabilityReport(activeVulnerabilityReportPayload)); 77 | 78 | const state = store.getState().scan; 79 | expect(state.filters).toEqual(filtersPayload); 80 | expect(state.activeCurrentFile).toBe(activeCurrentFilePayload); 81 | expect(state.activeVulnerabilityReport).toBe( 82 | activeVulnerabilityReportPayload 83 | ); 84 | }); 85 | 86 | describe('vulnerabilitySlice', () => { 87 | const initialState: VulReport = { 88 | currentFile: null, 89 | allFiles: null, 90 | }; 91 | 92 | it('should return the initial state', () => { 93 | expect(vulnerabilityReducer(undefined, { type: 'unknown' })).toEqual( 94 | initialState 95 | ); 96 | }); 97 | 98 | it('should handle getCurrentFileVulnerability', () => { 99 | const payload = { 100 | fileName: 'file1.js', 101 | vulnerabilities: ['vul1', 'vul2'], 102 | }; 103 | const action = getCurrentFileVulnerability(payload); 104 | 105 | const state = vulnerabilityReducer(initialState, action); 106 | 107 | expect(state.currentFile).toEqual(payload); 108 | expect(state.allFiles).toBeNull(); 109 | }); 110 | 111 | it('should handle getAllFilesVulnerability', () => { 112 | const payload = [ 113 | { fileName: 'file1.js', vulnerabilities: ['vul1'] }, 114 | { fileName: 'file2.js', vulnerabilities: ['vul2'] }, 115 | ]; 116 | const action = getAllFilesVulnerability(payload); 117 | 118 | const state = vulnerabilityReducer(initialState, action); 119 | 120 | expect(state.allFiles).toEqual(payload); 121 | expect(state.currentFile).toBeNull(); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /src/test/unitTest/webview/Organizationtable.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, fireEvent, screen } from '@testing-library/react'; 2 | import { Provider } from 'react-redux'; 3 | import { createStore } from 'redux'; 4 | 5 | import OrganizationTable from '../../../webview/components/Settings/OrganizationTable'; 6 | /* eslint-disable @typescript-eslint/no-explicit-any */ 7 | 8 | jest.mock('../../../webview/utils/postMessage', () => ({ 9 | webviewPostMessage: jest.fn(), 10 | })); 11 | const mockStore = (state: any) => createStore((state) => state, state); 12 | 13 | describe('OrganizationTable', () => { 14 | /* eslint-disable @typescript-eslint/no-explicit-any */ 15 | let store: any; 16 | 17 | beforeEach(() => { 18 | store = mockStore({ 19 | i10ln: { 20 | data: { 21 | organization: { 22 | organizationName: { translate: 'Org Name' }, 23 | projectName: { translate: 'Project Name' }, 24 | type: { translate: 'Type' }, 25 | }, 26 | tooltips: { 27 | edit: { translate: 'Edit' }, 28 | delete: { translate: 'Delete' }, 29 | }, 30 | }, 31 | }, 32 | project: { 33 | cancelStateWhileDelete: true, 34 | }, 35 | scan: { 36 | scanRetrievelDetectAcrossIds: false, 37 | }, 38 | assessFilter: { 39 | refreshBackgroundVulnRunnerAcrossIds: false, 40 | }, 41 | }); 42 | }); 43 | 44 | const defaultProps = { 45 | dataSource: [ 46 | { 47 | id: 1, 48 | organizationName: 'Org 1', 49 | projectName: 'Project 1', 50 | source: 'Type 1', 51 | }, 52 | { 53 | id: 2, 54 | organizationName: 'Org 2', 55 | projectName: 'Project 2', 56 | source: 'Type 2', 57 | }, 58 | ], 59 | onChange: jest.fn(), 60 | onDelete: jest.fn(), 61 | isDeselect: { deselectRow: false, updateDeselect: jest.fn() }, 62 | /* eslint-disable @typescript-eslint/no-explicit-any */ 63 | } as any; 64 | 65 | test('renders the table correctly', () => { 66 | render( 67 | 68 | 69 | 70 | ); 71 | 72 | expect(screen.getByText('Org Name')).toBeInTheDocument(); 73 | expect(screen.getByText('Project Name')).toBeInTheDocument(); 74 | expect(screen.getByText('Type')).toBeInTheDocument(); 75 | 76 | expect(screen.getByText('Org 1')).toBeInTheDocument(); 77 | expect(screen.getByText('Project 1')).toBeInTheDocument(); 78 | expect(screen.getByText('Type 1')).toBeInTheDocument(); 79 | expect(screen.getByText('Org 2')).toBeInTheDocument(); 80 | expect(screen.getByText('Project 2')).toBeInTheDocument(); 81 | expect(screen.getByText('Type 2')).toBeInTheDocument(); 82 | }); 83 | 84 | test('selects a row when clicked', () => { 85 | const testStore = mockStore({ 86 | ...store.getState(), 87 | project: { 88 | ...store.getState().project, 89 | cancelStateWhileDelete: false, 90 | }, 91 | }); 92 | render( 93 | 94 | 95 | 96 | ); 97 | 98 | const row = screen.getByText('Org 1').closest('tr'); 99 | fireEvent.click(row!); // Simulate a click on the row 100 | 101 | expect(row).toBeInTheDocument(); 102 | }); 103 | 104 | test('does not show edit or delete buttons when no row is selected', () => { 105 | render( 106 | 107 | 108 | 109 | ); 110 | 111 | const editButton = screen.queryByRole('button', { name: 'Edit' }); 112 | const deleteButton = screen.queryByRole('button', { name: 'Delete' }); 113 | 114 | expect(editButton).toBeNull(); 115 | expect(deleteButton).toBeNull(); 116 | }); 117 | 118 | test('calls updateDeselect when deselect is triggered', () => { 119 | const updatedProps = { 120 | ...defaultProps, 121 | isDeselect: { deselectRow: true, updateDeselect: jest.fn() }, 122 | }; 123 | 124 | render( 125 | 126 | 127 | 128 | ); 129 | 130 | expect(updatedProps.isDeselect.updateDeselect).toHaveBeenCalledWith(false); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /src/vscode-extension/utils/commandHandler/setting.handler.ts: -------------------------------------------------------------------------------- 1 | import { CommandRequest, ConfiguredProject } from '../../../common/types'; 2 | import { 3 | getAllApplicationsByOrgId, 4 | getAllProjectList, 5 | } from '../../api/services/apiService'; 6 | import { 7 | AddProjectToConfig, 8 | DeleteConfiguredProjectById, 9 | GetAllConfiguredProjects, 10 | UpdateConfiguredProjectById, 11 | } from '../../persistence/PersistenceConfigSetting'; 12 | import { WEBVIEW_COMMANDS, WEBVIEW_SCREENS } from '../constants/commands'; 13 | import { closeActiveFileHightlighting, tabBlocker } from '../helper'; 14 | import { 15 | assessBackgroundVulBehaviour, 16 | scanRetrievelBlocker, 17 | settingActionsBehaviour, 18 | updateGlobalWebviewConfig, 19 | } from '../multiInstanceConfigSync'; 20 | 21 | export const SettingCommandHandler = async (props: CommandRequest) => { 22 | const { command, payload } = props; 23 | switch (command) { 24 | case WEBVIEW_COMMANDS.SETTING_ADD_PROJECT_TO_CONFIGURE: { 25 | await scanRetrievelBlocker.disable(); 26 | await settingActionsBehaviour.disable(); 27 | const response = await AddProjectToConfig(payload as ConfiguredProject); 28 | await scanRetrievelBlocker.enable(); 29 | await updateGlobalWebviewConfig( 30 | WEBVIEW_SCREENS.SETTING, 31 | WEBVIEW_COMMANDS.SETTING_ADD_PROJECT_TO_CONFIGURE 32 | ); 33 | await settingActionsBehaviour.enable(); 34 | return { 35 | command: WEBVIEW_COMMANDS.SETTING_ADD_PROJECT_TO_CONFIGURE, 36 | data: response, 37 | }; 38 | } 39 | case WEBVIEW_COMMANDS.SETTING_GET_CONFIGURE_PROJECTS: { 40 | return { 41 | command: WEBVIEW_COMMANDS.SETTING_GET_CONFIGURE_PROJECTS, 42 | data: await GetAllConfiguredProjects(), 43 | }; 44 | } 45 | 46 | case WEBVIEW_COMMANDS.SETTING_GET_ALL_PROJECTS: { 47 | tabBlocker(false); 48 | const listOfProjects = await getAllProjectList( 49 | payload as ConfiguredProject 50 | ); 51 | tabBlocker(true); 52 | return { 53 | command: WEBVIEW_COMMANDS.SETTING_GET_ALL_PROJECTS, 54 | data: listOfProjects, 55 | }; 56 | } 57 | 58 | // New 59 | case WEBVIEW_COMMANDS.SETTING_GET_ALL_APPLICATIONS: { 60 | tabBlocker(false); 61 | const listOfProjects = await getAllApplicationsByOrgId( 62 | payload as ConfiguredProject 63 | ); 64 | tabBlocker(true); 65 | return { 66 | command: WEBVIEW_COMMANDS.SETTING_GET_ALL_APPLICATIONS, 67 | data: listOfProjects, 68 | }; 69 | } 70 | 71 | case WEBVIEW_COMMANDS.SETTING_UPDATE_CONFIGURE_PROJECT: { 72 | if (payload !== null && typeof payload === 'object' && 'id' in payload) { 73 | tabBlocker(false); 74 | await scanRetrievelBlocker.disable(); 75 | await settingActionsBehaviour.disable(); 76 | await assessBackgroundVulBehaviour.disable(); 77 | await closeActiveFileHightlighting(); 78 | 79 | const update = await UpdateConfiguredProjectById( 80 | payload.id as string, 81 | payload as ConfiguredProject 82 | ); 83 | await scanRetrievelBlocker.enable(); 84 | await assessBackgroundVulBehaviour.enable(); 85 | await settingActionsBehaviour.enable(); 86 | tabBlocker(true); 87 | return { 88 | command: WEBVIEW_COMMANDS.SETTING_UPDATE_CONFIGURE_PROJECT, 89 | data: update, 90 | }; 91 | } 92 | } 93 | 94 | case WEBVIEW_COMMANDS.SETTING_DELETE_CONFIGURE_PROJECT: { 95 | if (payload !== null && typeof payload === 'object' && 'id' in payload) { 96 | await settingActionsBehaviour.disable(); 97 | await scanRetrievelBlocker.disable(); 98 | if (payload.source === 'assess') { 99 | await assessBackgroundVulBehaviour.disable(); 100 | } 101 | const data = await DeleteConfiguredProjectById( 102 | payload.id as string, 103 | payload 104 | ); 105 | await scanRetrievelBlocker.enable(); 106 | await settingActionsBehaviour.enable(); 107 | await updateGlobalWebviewConfig( 108 | WEBVIEW_SCREENS.SETTING, 109 | WEBVIEW_COMMANDS.SETTING_DELETE_CONFIGURE_PROJECT, 110 | 'assessApplicationReload' 111 | ); 112 | await assessBackgroundVulBehaviour.enable(); 113 | 114 | return { 115 | command: WEBVIEW_COMMANDS.SETTING_DELETE_CONFIGURE_PROJECT, 116 | data: data, 117 | }; 118 | } 119 | } 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /src/test/unitTest/webview/messageHandler.test.tsx: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | messageHandler, 4 | ShowErrorPopup, 5 | ShowInformationPopup, 6 | ShowInformationPopupWithOptions, 7 | } from '../../../vscode-extension/commands/ui-commands/messageHandler'; 8 | 9 | import { CommandResponse } from '../../../common/types'; 10 | import { WEBVIEW_COMMANDS } from '../../../vscode-extension/utils/constants/commands'; 11 | 12 | jest.mock('vscode', () => ({ 13 | window: { 14 | showErrorMessage: jest.fn(), 15 | showInformationMessage: jest.fn(), 16 | }, 17 | })); 18 | 19 | // Mock the translations and other imported functionalities 20 | jest.mock('../../../l10n', () => ({ 21 | localeI18ln: { 22 | getTranslation: jest 23 | .fn() 24 | .mockReturnValue('No vulnerabilities found for the selected filters'), 25 | }, 26 | })); 27 | 28 | describe('UI Commands - messageHandler', () => { 29 | beforeEach(() => { 30 | jest.clearAllMocks(); 31 | }); 32 | 33 | it('should show error popup when command response is failure', () => { 34 | const res: CommandResponse = { 35 | command: WEBVIEW_COMMANDS.SCAN_MANUAL_REFRESH, 36 | data: { 37 | status: 'failure', 38 | message: 'Scan failed', 39 | }, 40 | }; 41 | 42 | messageHandler(res); 43 | 44 | expect(vscode.window.showErrorMessage).toHaveBeenCalledWith('Scan failed'); 45 | expect(vscode.window.showInformationMessage).not.toHaveBeenCalled(); 46 | }); 47 | 48 | it('should show information popup with translated message when command response is success and no vulnerabilities found', () => { 49 | const res: CommandResponse = { 50 | command: WEBVIEW_COMMANDS.SCAN_MANUAL_REFRESH, 51 | data: { 52 | status: 'success', 53 | message: 'Scan successful', 54 | code: 200, 55 | responseData: { 56 | child: [], // Empty responseData array signifies no vulnerabilities found 57 | }, 58 | }, 59 | }; 60 | 61 | messageHandler(res); 62 | 63 | // Check if the translated message for "No vulnerabilities found" is shown 64 | expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( 65 | 'No vulnerabilities found for the selected filters' 66 | ); 67 | expect(vscode.window.showErrorMessage).not.toHaveBeenCalled(); 68 | }); 69 | 70 | it('should show information popup when command response is success and vulnerabilities are found', () => { 71 | const res: CommandResponse = { 72 | command: WEBVIEW_COMMANDS.SCAN_MANUAL_REFRESH, 73 | data: { 74 | status: 'success', 75 | message: 'Scan successful', 76 | code: 200, 77 | responseData: { 78 | child: [{}, {}, {}], // Non-empty array signifies vulnerabilities found 79 | }, 80 | }, 81 | }; 82 | 83 | messageHandler(res); 84 | 85 | expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( 86 | 'Scan successful' 87 | ); 88 | expect(vscode.window.showErrorMessage).not.toHaveBeenCalled(); 89 | }); 90 | 91 | it('should not show error or info when command is not in messageCommands list', () => { 92 | const res: CommandResponse = { 93 | command: 'UNKNOWN_COMMAND', 94 | data: { 95 | status: 'success', 96 | message: 'This should not be handled', 97 | }, 98 | }; 99 | 100 | const result = messageHandler(res); 101 | 102 | expect(vscode.window.showInformationMessage).not.toHaveBeenCalled(); 103 | expect(vscode.window.showErrorMessage).not.toHaveBeenCalled(); 104 | expect(result).toBe(true); 105 | }); 106 | 107 | it('should show information popup with options when ShowInformationPopupWithOptions is called', () => { 108 | const message = 'Do you want to continue?'; 109 | 110 | ShowInformationPopupWithOptions(message); 111 | 112 | expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( 113 | message, 114 | 'Yes', 115 | 'no' 116 | ); 117 | }); 118 | 119 | it('should show error popup correctly when ShowErrorPopup is called', () => { 120 | const message = 'An error occurred!'; 121 | 122 | ShowErrorPopup(message); 123 | 124 | expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(message); 125 | }); 126 | 127 | it('should show information popup correctly when ShowInformationPopup is called', () => { 128 | const message = 'Operation successful!'; 129 | 130 | ShowInformationPopup(message); 131 | 132 | expect(vscode.window.showInformationMessage).toHaveBeenCalledWith(message); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [3.0.2] - 2025-12-15 4 | 5 | ### Added 6 | 7 | - CI/CD pipeline for automatic deployment of the extension to the OpenVSX Registry and Microsoft Marketplace. 8 | 9 | ## [3.0.1] - 2025-12-08 10 | 11 | ### Added 12 | 13 | - Extended support for the Gitpod Web Browser IDE. 14 | - Added support for the AutoRemediated status in the assess filter screen. 15 | 16 | ### Changed 17 | 18 | - Removed the old mechanism of encrypting and decrypting API Key and Service Key using the crypto library. The extension now uses VS Code Secret Storage for secure credential storage and retrieval. 19 | - Updated the About page to retrieve dynamic platform-based data. 20 | 21 | ### Fixed 22 | 23 | - Fixed several bugs related to text updates in pop-up messages. 24 | - Fixed the timestamp issue in the logs; it now shows the exact start time and end time of the service from initiation until a response is returned. 25 | 26 | --- 27 | 28 | ## [3.0.0] - 2025-07-31 29 | 30 | ### Added 31 | 32 | - Support for Contrast Assess Software Composition Analysis (SCA). 33 | - Connect to your Contrast organization and retrieve a list of library vulnerabilities (SCA). 34 | - Retrieve and display SCA vulnerabilities directly within the IDE. 35 | - View detailed information for each vulnerability: Libraries include `Overview`, `How to Fix`, `Path`, and `Tags` tabs and **redirection**; CVEs display the `Overview` tab and **redirection**. 36 | - View the file path of libraries found in manifest files across supported languages in the `Library Path` tab. 37 | - Filter the list of SCA vulnerabilities by `Severity (SCA)`, `Status (SCA)`, `Environment`, `Server`, `Quick View`, `Library Usage`, `License Type`, and `Tags`. 38 | - Set and modify tags for library vulnerabilities (SCA). 39 | - Contextual redirection to: 40 | - `NVD` and `CVE Record` for public vulnerability information. 41 | - Contrast TeamServer for additional vulnerability details. 42 | - Support for scheduled vulnerability retrieval based on configured intervals. 43 | 44 | ### Changed 45 | 46 | - Introduced a unified `Run` button in the IDE to fetch both Assess and SCA vulnerabilities from the Contrast TeamServer. 47 | - Redesigned the filtering experience UI with dedicated filter tabs for Assess and SCA, maintaining separate session filters under the Filter screen. 48 | - Additionally, added `Tags` and `Environments` fields to the Vulnerability Filter screen and realigned the filter fields. 49 | - Removed manual refresh fetaure for assess. 50 | 51 | --- 52 | 53 | ## [2.0.0] - 2025-05-19 54 | 55 | ### Added 56 | 57 | - Support for Contrast Assess Interactive Application Security Testing (IAST). 58 | - Connect to your Contrast organization and retrieve a list of applications. 59 | - Retrieve vulnerabilities (IAST) and display them in the IDE 60 | - Ability to see which line of code has a vulnerability through clear indicators 61 | - Ability to hover over the affected line of code to see a short description and severity of the vulnerability 62 | - Added indicators on screen of the total number of vulnerabilities, by severity, in the file you have opened 63 | - Filter the list of vulnerabilities retrieved by `severity (IAST)`, `Status (IAST)` and by `session metadata (IAST)` 64 | - Set tags (IAST) and change status of a vulnerability (IAST) 65 | - Manually refresh the vulnerabilities of the application. 66 | - Fetch the Vulnerability based on the scheduled duration. 67 | - Contextual Redirection to Contrast TeamServer to get additional vulnerability details. 68 | - Restrict retrieval of vulnerabilities for archived projects or applications. 69 | 70 | --- 71 | 72 | ## [1.0.0] - 2025-01-06 73 | 74 | ### Added 75 | 76 | - Initial release of **Contrast IDE**, a VS Code extension for scanning the project vulnerabilities. 77 | - Support for code analysis in `Java`, `JavaScript`, `TypeScript`, `C++`, `C#`, `Python`, and `PHP`. 78 | - Compatibility with `Windows` (Windows 11), `Linux` (Ubuntu 22.04.5 LTS) platforms. 79 | - Support for Contrast Scan Static Application Security Testing (SAST) with the following capabilities: 80 | - Connect to your Contrast Organisation and obtain a list of Projects (SAST) 81 | - Retrieve vulnerabilities and display them in the IDE 82 | - To see which line of code has a vulnerability through visual indicators 83 | - To hover over the affected line of code to see a short description and severity of the vulnerability 84 | - On-screen indicators display the total number of vulnerabilities, categorized by severity in the file you have opened 85 | - Filter the list of vulnerabilities retrieved by severity and status 86 | - Ability to connect using multiple set of configurable connection parameters, ensuring flexibility for various endpoints or environments 87 | - `Contrast URL` 88 | - `User Name` 89 | - `Organization ID` 90 | - `API Key` 91 | - `Service Key` 92 | - Flexibility for various endpoints in team server environments 93 | - IDE support is currently limited to Visual Studio Code (versions `1.93.0` and above). 94 | -------------------------------------------------------------------------------- /src/webview/screens/Assess/tabs/VulnerabilityReport/VulnerabilityReport.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | AssessProjectVulnerability, 4 | AssessVulnerability, 5 | ContrastAssessLocale, 6 | ReducerTypes, 7 | } from '../../../../../common/types'; 8 | 9 | import { useSelector } from 'react-redux'; 10 | import { AssessVulnerabilityReport } from '../../../../components/Assess/VulnerabilityReport'; 11 | import { SplitWindow } from './SplitWindow'; 12 | import { webviewPostMessage } from '../../../../utils/postMessage'; 13 | import { 14 | WEBVIEW_COMMANDS, 15 | WEBVIEW_SCREENS, 16 | } from '../../../../../vscode-extension/utils/constants/commands'; 17 | import { AssessEventsHttpRequestUpdate } from '../../../../utils/helper'; 18 | 19 | const isVulnerabilityInList = ( 20 | list: AssessProjectVulnerability, 21 | vul: AssessVulnerability 22 | ): boolean => { 23 | if (vul === undefined || vul === null) { 24 | return false; 25 | } 26 | return ( 27 | list.child 28 | .find((item) => item.label === vul.labelForMapping) 29 | ?.child?.some((item1) => item1.label === vul.label) ?? false 30 | ); 31 | }; 32 | function vulnerabilityReport() { 33 | const i18nData = useSelector((state: ReducerTypes) => state.i10ln.data); 34 | const vulnerabilitiesList = useSelector( 35 | (state: ReducerTypes) => state.assessFilter.allFiles 36 | ); 37 | const [i18nFields, updateI18nFields] = useState( 38 | '

    No vulnerabilities found:

    1. Navigate to the Contrast - Assess menu
    2. Select the necessary filters and click Run Button.
    3. View the results in the Vulnerability Report tab.

    After retrieving vulnerabilities, return to this screen or else click on refresh icon to see the latest vulnerability report.

    ' 39 | ); 40 | 41 | const [allfileVul, setAllFileVul] = useState( 42 | [] 43 | ); 44 | const [getSelectedVul, setSelectedVul] = useState<{ 45 | fetching: null | AssessVulnerability; 46 | }>({ 47 | fetching: null, 48 | }); 49 | 50 | useEffect(() => { 51 | if ( 52 | vulnerabilitiesList !== undefined && 53 | vulnerabilitiesList !== null && 54 | vulnerabilitiesList.responseData !== null && 55 | vulnerabilitiesList.responseData !== undefined 56 | ) { 57 | const node = 58 | vulnerabilitiesList.responseData as AssessProjectVulnerability; 59 | setAllFileVul([node]); 60 | if (getSelectedVul.fetching !== null) { 61 | if (isVulnerabilityInList(node, getSelectedVul.fetching) === false) { 62 | setSelectedVul({ fetching: null }); 63 | } 64 | } 65 | } else { 66 | setAllFileVul([]); 67 | setSelectedVul({ fetching: null }); 68 | } 69 | }, [vulnerabilitiesList]); 70 | 71 | useEffect(() => { 72 | if (vulnerabilitiesList === null || vulnerabilitiesList?.code === 400) { 73 | webviewPostMessage({ 74 | command: WEBVIEW_COMMANDS.ASSESS_GET_INITIAL_ALL_FILES_VULNERABILITY, 75 | payload: null, 76 | screen: WEBVIEW_SCREENS.ASSESS, 77 | }); 78 | } 79 | }, [vulnerabilitiesList]); 80 | 81 | useEffect(() => { 82 | if (i18nData !== null && i18nData !== null) { 83 | const { vulnerabilityReport } = 84 | i18nData as unknown as ContrastAssessLocale; 85 | updateI18nFields(vulnerabilityReport?.htmlElements?.translate as string); 86 | } 87 | }, [i18nData]); 88 | 89 | const setStyle = (width: 'full' | 'half') => { 90 | return { width: width === 'full' ? '100%' : '50%' }; 91 | }; 92 | 93 | const handleVulnerabilitySelect = (e: AssessVulnerability) => { 94 | AssessEventsHttpRequestUpdate(e); 95 | setSelectedVul({ fetching: e }); 96 | }; 97 | 98 | return ( 99 | <> 100 |

    101 |
    102 | {allfileVul.length === 0 || allfileVul === null ? ( 103 |
    107 | ) : ( 108 | { 111 | if (e?.isUnmapped === false) { 112 | webviewPostMessage({ 113 | command: WEBVIEW_COMMANDS.ASSESS_OPEN_VULNERABILITY_FILE, 114 | payload: e, 115 | screen: WEBVIEW_SCREENS.ASSESS, 116 | }); 117 | } 118 | 119 | if (e?.level === 0) { 120 | handleVulnerabilitySelect(e); 121 | } 122 | }} 123 | /> 124 | )} 125 |
    126 | {getSelectedVul.fetching !== null && 127 | allfileVul.length > 0 && 128 | allfileVul[0]?.child?.length > 0 ? ( 129 |
    130 | 131 |
    132 | ) : null} 133 |
    134 | 135 | ); 136 | } 137 | 138 | export default vulnerabilityReport; 139 | -------------------------------------------------------------------------------- /src/test/unitTest/vscode-extension/getOrganisationName.test.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | import { getOrganisationName } from '../../../vscode-extension/api/services/apiService'; // Adjust import based on your structure 4 | import { authBase64 } from '../../../webview/utils/authBase64'; 5 | import path from 'path'; 6 | import { Uri } from 'vscode'; 7 | import { loggerInstance } from '../../../vscode-extension/logging/logger'; 8 | 9 | jest.mock('../../../webview/utils/authBase64', () => ({ 10 | authBase64: jest.fn(), 11 | })); 12 | 13 | jest.mock('vscode', () => { 14 | const UIKind = { Desktop: 1, Web: 2 }; 15 | return { 16 | UIKind, 17 | env: { 18 | language: 'en', 19 | appName: 'VSCode', 20 | uiKind: UIKind.Desktop, 21 | }, 22 | workspace: { 23 | workspaceFolders: [{ uri: { fsPath: '/path/to/mock/workspace' } }], 24 | onDidChangeConfiguration: jest.fn(), 25 | }, 26 | window: { 27 | activeTextEditor: { 28 | document: { 29 | fileName: 'test.js', 30 | }, 31 | }, 32 | createTreeView: jest.fn().mockReturnValue({ 33 | onDidChangeVisibility: jest.fn(), 34 | }), 35 | }, 36 | TreeItem: class { 37 | [x: string]: { dark: Uri; light: Uri }; 38 | constructor( 39 | label: { dark: Uri; light: Uri }, 40 | /* eslint-disable @typescript-eslint/no-explicit-any */ 41 | command: any = null, 42 | /* eslint-disable @typescript-eslint/no-explicit-any */ 43 | icon: any = null 44 | ) { 45 | this.label = label; 46 | if (command !== null) { 47 | this.command = { 48 | title: label, 49 | command: command, 50 | } as any; 51 | } 52 | if (icon !== null) { 53 | const projectRoot = path.resolve(__dirname, '..'); 54 | const iconPath = Uri.file(path.join(projectRoot, 'assets', icon)); 55 | this.iconPath = { 56 | dark: iconPath, 57 | light: iconPath, 58 | }; 59 | } 60 | } 61 | }, 62 | Uri: { 63 | file: jest.fn().mockReturnValue('mockUri'), 64 | }, 65 | commands: { 66 | registerCommand: jest.fn(), 67 | }, 68 | languages: { 69 | registerHoverProvider: jest.fn(), 70 | }, 71 | }; 72 | }); 73 | 74 | jest.mock( 75 | '../../../vscode-extension/commands/ui-commands/webviewHandler', 76 | () => ({ 77 | ContrastPanelInstance: { 78 | postMessage: jest.fn(), 79 | }, 80 | }) 81 | ); 82 | 83 | jest.mock('../../../vscode-extension/api/services/apiService', () => ({ 84 | ...jest.requireActual('../../../vscode-extension/api/services/apiService'), 85 | getAxiosClient: jest.fn(), 86 | })); 87 | 88 | jest.mock('../../../vscode-extension/logging/logger', () => ({ 89 | loggerInstance: { 90 | logMessage: jest.fn(), 91 | }, 92 | })); 93 | 94 | describe('getOrganisationName', () => { 95 | let mockAxios: MockAdapter; 96 | let mockGetAxiosClient: jest.Mock; 97 | 98 | const mockConfig = { 99 | apiKey: 'fakeApiKey', 100 | contrastURL: 'https://local.com', 101 | userName: 'testUser', 102 | serviceKey: 'fakeServiceKey', 103 | organizationId: 'org123', 104 | source: 'someSource', 105 | projectName: 'TestProject', 106 | minute: 5, 107 | }; 108 | 109 | beforeEach(() => { 110 | mockAxios = new MockAdapter(axios as any); 111 | mockGetAxiosClient = jest.fn(); 112 | mockGetAxiosClient.mockReturnValue(axios); 113 | }); 114 | 115 | afterEach(() => { 116 | mockAxios.restore(); 117 | }); 118 | 119 | it('should return the organization name when the API call is successful', async () => { 120 | const mockConfig = { 121 | apiKey: 'fakeApiKey', 122 | contrastURL: 'https://local.com', 123 | userName: 'testUser', 124 | serviceKey: 'fakeServiceKey', 125 | organizationId: 'org123', 126 | source: 'someSource', 127 | projectName: 'TestProject', 128 | minute: 5, 129 | }; 130 | 131 | mockAxios 132 | .onGet(`/ng/profile/organizations/${mockConfig.organizationId}`) 133 | .reply(200, { 134 | organization: { name: 'Test Organization' }, 135 | }); 136 | 137 | (authBase64 as jest.Mock).mockReturnValue('mockedAuthHeader'); 138 | 139 | const result = await getOrganisationName(mockConfig); 140 | 141 | expect(result).toBe('Test Organization'); 142 | }); 143 | 144 | it('should return false when the API call fails', async () => { 145 | mockAxios 146 | .onGet(`/ng/profile/organizations/${mockConfig.organizationId}`) 147 | .reply(500); 148 | 149 | const result = await getOrganisationName(mockConfig); 150 | 151 | expect(loggerInstance.logMessage).toHaveBeenCalledTimes(1); 152 | 153 | expect(result).toBe(false); 154 | }); 155 | 156 | it('should return false if no organization name is found in the response', async () => { 157 | mockAxios 158 | .onGet(`/ng/profile/organizations/${mockConfig.organizationId}`) 159 | .reply(200, {}); 160 | 161 | const result = await getOrganisationName(mockConfig); 162 | 163 | expect(result).toBe(false); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /src/vscode-extension/logging/logger.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as vscode from 'vscode'; 4 | import { LogLevel } from '../../common/types'; 5 | import { extractLastNumber } from '../utils/commonUtil'; 6 | 7 | const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5 MB 8 | const MAX_LOG_FILES = 10; // Maximum 10 files to keep 9 | 10 | export class Logger { 11 | private logDir!: string; 12 | private logFileName!: string; 13 | private logFilePath!: string; 14 | 15 | constructor(private context: vscode.ExtensionContext) { 16 | const listOfWorkspaceFolder = vscode.workspace; 17 | if ( 18 | !listOfWorkspaceFolder?.workspaceFolders || 19 | listOfWorkspaceFolder.workspaceFolders.length === 0 20 | ) { 21 | console.error('Workspace folder not found. Logs cannot be saved.'); 22 | return; 23 | } 24 | const workspaceFolder = listOfWorkspaceFolder.workspaceFolders?.[0]; 25 | 26 | this.logDir = path.join( 27 | workspaceFolder.uri.fsPath, 28 | '.vscode', 29 | 'logs', 30 | 'contrast_scan_vulplugin' 31 | ); 32 | this.ensureLogDirExists(); 33 | this.logFileName = `contrast_scan_vulplugin-${this.getFormattedDate()}.log`; 34 | this.logFilePath = path.join(this.logDir, this.logFileName); 35 | void this.rotateLogs(); 36 | } 37 | 38 | private ensureLogDirExists() { 39 | if (!fs.existsSync(this.logDir)) { 40 | fs.mkdirSync(this.logDir, { recursive: true }); // Create the directory if it doesn't exist 41 | } 42 | } 43 | 44 | private getFormattedDate(): string { 45 | return new Date().toISOString().slice(0, 10); // YYYY-MM-DD format 46 | } 47 | 48 | private async log( 49 | level: LogLevel, 50 | message: string, 51 | metadata?: { size?: string; records?: number; responseTime?: string } 52 | ): Promise { 53 | let logEntry = `[${new Date().toISOString().replace('T', ' ').substring(0, 19)}] [${level}] - ${message}`; 54 | 55 | if (metadata) { 56 | const { size, records, responseTime } = metadata; 57 | const metadataEntries = []; 58 | if (size !== null && size !== undefined) { 59 | metadataEntries.push(`Size: ${size}`); 60 | } 61 | if (records !== null && records !== undefined) { 62 | metadataEntries.push(`Records: ${records}`); 63 | } 64 | if (responseTime !== null && records !== undefined) { 65 | metadataEntries.push(`Response Time: ${responseTime}`); 66 | } 67 | 68 | if (metadataEntries.length > 0) { 69 | logEntry += ` | ${metadataEntries.join(' | ')}`; 70 | } 71 | } 72 | 73 | logEntry += '\n'; 74 | await this.checkLogRotation(); 75 | await fs.promises.appendFile(this.logFilePath, logEntry); 76 | } 77 | 78 | public async logMessage( 79 | level: LogLevel, 80 | message: string, 81 | metadata?: { size?: string; records?: number; responseTime?: string } 82 | ): Promise { 83 | await this.log(level, message, metadata); 84 | } 85 | 86 | private async checkLogRotation(): Promise { 87 | try { 88 | const stats = await fs.promises.stat(this.logFilePath); 89 | if (stats.size > MAX_LOG_SIZE) { 90 | await this.rotateLogs(); 91 | } 92 | } catch { 93 | console.error('Error checking log file size:'); 94 | } 95 | } 96 | 97 | private async rotateLogs(): Promise { 98 | const logFiles = await fs.promises.readdir(this.logDir); 99 | const currentLogFile = path.join(this.logDir, this.logFileName); 100 | const logFileRegex = new RegExp( 101 | `^${this.logFileName.replace(/\.log$/, '')}(_\d+)?\.log$` 102 | ); 103 | 104 | const sortedLogFiles = logFiles 105 | .filter((file) => logFileRegex.test(file)) 106 | .sort((a, b) => { 107 | const aNum = extractLastNumber(a); 108 | const bNum = extractLastNumber(b); 109 | return aNum - bNum; 110 | }); 111 | 112 | if (fs.existsSync(currentLogFile)) { 113 | const rotatedLog = path.join( 114 | this.logDir, 115 | `${this.logFileName.replace(/\.log$/, '')}_1.log` 116 | ); 117 | await fs.promises.rename(currentLogFile, rotatedLog); 118 | } 119 | 120 | for (let i = MAX_LOG_FILES - 1; i > 0; i--) { 121 | const prevLog = path.join( 122 | this.logDir, 123 | `${this.logFileName.replace(/\.log$/, '')}_${i}.log` 124 | ); 125 | const nextLog = path.join( 126 | this.logDir, 127 | `${this.logFileName.replace(/\.log$/, '')}_${i + 1}.log` 128 | ); 129 | if (fs.existsSync(prevLog)) { 130 | await fs.promises.rename(prevLog, nextLog); 131 | } 132 | } 133 | 134 | if (sortedLogFiles.length > MAX_LOG_FILES) { 135 | const filesToDelete = sortedLogFiles.slice(MAX_LOG_FILES); 136 | await Promise.all( 137 | filesToDelete.map((file) => 138 | fs.promises.unlink(path.join(this.logDir, file)) 139 | ) 140 | ); 141 | } 142 | } 143 | } 144 | 145 | // Export the logger instance 146 | let loggerInstance: Logger; 147 | 148 | export const initializeLogger = (context: vscode.ExtensionContext) => { 149 | loggerInstance = new Logger(context); 150 | }; 151 | 152 | export { loggerInstance }; 153 | -------------------------------------------------------------------------------- /src/test/unitTest/vscode-extension/getScanResults.test.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { getScanResults } from '../../../vscode-extension/api/services/apiService'; 3 | import { 4 | resolveFailure, 5 | resolveSuccess, 6 | } from '../../../vscode-extension/utils/errorHandling'; 7 | import { PersistenceInstance } from '../../../vscode-extension/utils/persistanceState'; 8 | import { configuredProject1, configuredProject2 } from '../../mocks/testMock'; 9 | import { ApiResponse } from '../../../common/types'; 10 | 11 | jest.mock('axios'); 12 | jest.mock('../../../vscode-extension/utils/errorHandling'); 13 | jest.mock('../../../vscode-extension/utils/persistanceState'); 14 | jest.mock('../../../vscode-extension/logging/cacheLogger'); 15 | jest.mock('../../../vscode-extension/cache/backgroundRefreshTimer'); 16 | jest.mock('../../../l10n'); 17 | const mockedAxios = axios as jest.Mocked; 18 | 19 | const mockedResolveFailure = resolveFailure as jest.MockedFunction< 20 | typeof resolveFailure 21 | >; 22 | const mockedResolveSuccess = resolveSuccess as jest.MockedFunction< 23 | typeof resolveSuccess 24 | >; 25 | 26 | jest.mock('../../../vscode-extension/utils/persistanceState', () => ({ 27 | PersistenceInstance: { 28 | set: jest.fn(), 29 | getByKey: jest.fn(), 30 | }, 31 | })); 32 | 33 | jest.mock('../../../vscode-extension/utils/encryptDecrypt', () => ({ 34 | encrypt: jest.fn((key) => `encrypted-${key}`), 35 | })); 36 | 37 | jest.mock('../../../vscode-extension/api/services/apiService', () => { 38 | return { 39 | getScanResults: jest.fn(), 40 | }; 41 | }); 42 | 43 | describe('getScanResults', () => { 44 | it('should handle successful response from API', async () => { 45 | const mockProjectData = [configuredProject1, configuredProject2]; 46 | 47 | const mockResponse: ApiResponse = { 48 | responseData: [], 49 | code: 200, 50 | status: 'success', 51 | message: 'Scan results fetched successfully', 52 | }; 53 | ( 54 | getScanResults as jest.MockedFunction 55 | ).mockResolvedValueOnce(mockResponse); 56 | 57 | (PersistenceInstance.getByKey as jest.Mock).mockReturnValue( 58 | mockProjectData 59 | ); 60 | 61 | mockedResolveSuccess.mockReturnValue({ 62 | message: 'Scan results fetched successfully', 63 | code: 200, 64 | status: 'success', 65 | responseData: [], 66 | }); 67 | 68 | mockedAxios.get.mockResolvedValueOnce({ 69 | status: 200, 70 | data: { 71 | content: [], 72 | totalPages: 1, 73 | }, 74 | }); 75 | 76 | const response = await getScanResults('123'); 77 | expect(response.status).toBe('success'); 78 | }); 79 | 80 | it('should handle API failure response', async () => { 81 | const mockProjectData = [configuredProject1, configuredProject2]; 82 | 83 | const mockResponse: ApiResponse = { 84 | responseData: [], 85 | code: 500, 86 | status: 'failure', 87 | message: 'Error fetching scan results', 88 | }; 89 | ( 90 | getScanResults as jest.MockedFunction 91 | ).mockResolvedValueOnce(mockResponse); 92 | 93 | (PersistenceInstance.getByKey as jest.Mock).mockReturnValue( 94 | mockProjectData 95 | ); 96 | mockedAxios.get.mockRejectedValue(new Error('Failed to fetch')); 97 | 98 | const response = await getScanResults('123'); 99 | expect(response).toEqual({ 100 | code: 500, 101 | message: 'Error fetching scan results', 102 | responseData: [], 103 | status: 'failure', 104 | }); 105 | expect(mockedResolveFailure).toHaveBeenCalledTimes(0); 106 | }); 107 | 108 | it('should handle scenario when results are paginated and multiple pages are fetched', async () => { 109 | const mockProjectData = [configuredProject1, configuredProject2]; 110 | 111 | const mockResponse: ApiResponse = { 112 | responseData: [], 113 | code: 200, 114 | status: 'success', 115 | message: 'Scan results fetched successfully', 116 | }; 117 | ( 118 | getScanResults as jest.MockedFunction 119 | ).mockResolvedValueOnce(mockResponse); 120 | 121 | (PersistenceInstance.getByKey as jest.Mock).mockReturnValue( 122 | mockProjectData 123 | ); 124 | 125 | mockedAxios.get 126 | .mockResolvedValueOnce({ 127 | status: 200, 128 | data: { 129 | content: [{ name: 'SQL Injection', severity: 'CRITICAL' }], 130 | totalPages: 2, 131 | }, 132 | }) 133 | .mockResolvedValueOnce({ 134 | status: 200, 135 | data: { 136 | content: [{ name: 'Cross-Site Scripting', severity: 'HIGH' }], 137 | totalPages: 2, 138 | }, 139 | }); 140 | 141 | mockedResolveSuccess.mockReturnValue({ 142 | message: 'Scan results fetched successfully', 143 | code: 200, 144 | status: 'success', 145 | responseData: [ 146 | { name: 'SQL Injection', severity: 'CRITICAL' }, 147 | { name: 'Cross-Site Scripting', severity: 'HIGH' }, 148 | ], 149 | }); 150 | 151 | const response = await getScanResults('123'); 152 | expect(response.status).toEqual('success'); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /src/vscode-extension/commands/ui-commands/aboutWebviewHandler.ts: -------------------------------------------------------------------------------- 1 | import { window, ViewColumn, WebviewPanel, commands, Uri } from 'vscode'; 2 | import { CONSTRAST_ABOUT } from '../../utils/constants/commands'; 3 | import { globalExtentionUri } from '..'; 4 | import { PathResolver } from '../../utils/pathResolver'; 5 | import { getPackageInformation } from '../../api/services/apiService'; 6 | import { PackageInfo } from '../../../common/types'; 7 | 8 | let webviewPanel: WebviewPanel | undefined; 9 | let instanceCount: number = 0; 10 | 11 | class AboutWebviewPanel { 12 | constructor() {} 13 | 14 | async init() { 15 | if (webviewPanel && instanceCount > 0) { 16 | webviewPanel.reveal(ViewColumn.One); 17 | return; 18 | } 19 | 20 | instanceCount += 1; 21 | 22 | webviewPanel = window.createWebviewPanel( 23 | CONSTRAST_ABOUT, 24 | 'About', 25 | ViewColumn.One, 26 | { 27 | enableScripts: true, 28 | localResourceRoots: [globalExtentionUri.extensionUri], 29 | } 30 | ); 31 | 32 | webviewPanel.onDidDispose(() => { 33 | instanceCount--; 34 | webviewPanel = undefined; 35 | }); 36 | 37 | this.setWebviewIcon(); 38 | await this.render(); 39 | } 40 | 41 | private setWebviewIcon(): void { 42 | webviewPanel !== null && webviewPanel !== undefined 43 | ? (webviewPanel.iconPath = Uri.joinPath( 44 | globalExtentionUri.extensionUri, 45 | 'assets', 46 | 'CS_logo_white_bg.jpg' 47 | )) 48 | : null; 49 | } 50 | 51 | private async tabular(): Promise { 52 | const packageInformation = await getPackageInformation(); 53 | 54 | if (packageInformation !== null && packageInformation !== undefined) { 55 | const { code, responseData } = packageInformation; 56 | const { 57 | IDEVersion, 58 | aboutPage, 59 | displayName, 60 | osWithVersion, 61 | platform, 62 | version, 63 | } = responseData as PackageInfo; 64 | 65 | if (code === 200 && responseData !== null && responseData !== undefined) { 66 | return ` 67 |

    68 | Contrast Plugin 69 |

    70 |
    71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
    Plugin Name${displayName}
    Plugin Release Version${version}
    IDE Version${IDEVersion}
    OS Version${osWithVersion}
    Platform${platform}
    96 |
    97 |
    98 |

    99 | ${aboutPage.title} 100 |

    101 |
    102 | ${aboutPage.content} 103 |
    104 | `; 105 | } 106 | } 107 | return null; 108 | } 109 | 110 | private async template(): Promise { 111 | let stylePath: Uri | undefined; 112 | 113 | if (webviewPanel) { 114 | const pathResolver = new PathResolver(webviewPanel.webview); 115 | stylePath = pathResolver.resolve(['src', 'styles', 'about.css']); 116 | } 117 | 118 | return ` 119 | 120 | 121 | 122 | 123 | 124 | ${stylePath ? `` : ''} 125 | 126 | 127 | ${await this.tabular()} 128 | 129 | 130 | `; 131 | } 132 | 133 | private async render(): Promise { 134 | if (webviewPanel) { 135 | webviewPanel.webview.html = await this.template(); 136 | } 137 | } 138 | 139 | public dispose() { 140 | if (webviewPanel) { 141 | webviewPanel.dispose(); 142 | } 143 | } 144 | } 145 | 146 | const aboutWebviewPanelInstance = new AboutWebviewPanel(); 147 | export const registerAboutWebviewPanel = commands.registerCommand( 148 | CONSTRAST_ABOUT, 149 | async () => { 150 | await aboutWebviewPanelInstance.init(); 151 | } 152 | ); 153 | 154 | export { aboutWebviewPanelInstance }; 155 | -------------------------------------------------------------------------------- /src/test/unitTest/vscode-extension/getVulnerabilityByTraceId.test.ts: -------------------------------------------------------------------------------- 1 | import { getDataOnlyFromCacheAssess } from '../../../vscode-extension/cache/cacheManager'; 2 | import { localeI18ln } from '../../../l10n'; 3 | import { 4 | resolveFailure, 5 | resolveSuccess, 6 | } from '../../../vscode-extension/utils/errorHandling'; 7 | import { getVulnerabilityByTraceId } from '../../../vscode-extension/api/services/apiService'; 8 | import path from 'path'; 9 | import { Uri } from 'vscode'; 10 | 11 | jest.mock('../../../vscode-extension/cache/cacheManager', () => ({ 12 | getDataOnlyFromCacheAssess: jest.fn(), 13 | })); 14 | 15 | jest.mock('../../../l10n', () => ({ 16 | localeI18ln: { 17 | getTranslation: jest.fn(), 18 | }, 19 | })); 20 | 21 | jest.mock('../../../vscode-extension/utils/errorHandling', () => ({ 22 | resolveFailure: jest.fn(), 23 | resolveSuccess: jest.fn(), 24 | })); 25 | 26 | /* eslint-disable @typescript-eslint/no-explicit-any */ 27 | jest.mock('vscode', () => { 28 | const UIKind = { Desktop: 1, Web: 2 }; 29 | return { 30 | UIKind, 31 | env: { 32 | language: 'en', 33 | appName: 'VSCode', 34 | uiKind: UIKind.Desktop, 35 | }, 36 | workspace: { 37 | workspaceFolders: [{ uri: { fsPath: '/path/to/mock/workspace' } }], 38 | onDidChangeConfiguration: jest.fn(), 39 | }, 40 | window: { 41 | createTreeView: jest.fn().mockReturnValue({ 42 | onDidChangeVisibility: jest.fn(), 43 | }), 44 | activeTextEditor: null, 45 | }, 46 | 47 | TreeItem: class { 48 | [x: string]: { dark: Uri; light: Uri }; 49 | constructor( 50 | label: { dark: Uri; light: Uri }, 51 | command: any = null, 52 | icon: any = null 53 | ) { 54 | this.label = label; 55 | if (command !== null) { 56 | this.command = { 57 | title: label, 58 | command: command, 59 | } as any; 60 | } 61 | if (icon !== null) { 62 | const projectRoot = path.resolve(__dirname, '..'); 63 | const iconPath = Uri.file(path.join(projectRoot, 'assets', icon)); 64 | this.iconPath = { 65 | dark: iconPath, 66 | light: iconPath, 67 | }; 68 | } 69 | } 70 | }, 71 | Uri: { 72 | file: jest.fn().mockReturnValue('mockUri'), 73 | }, 74 | commands: { 75 | registerCommand: jest.fn(), 76 | }, 77 | languages: { 78 | registerHoverProvider: jest.fn(), 79 | }, 80 | }; 81 | }); 82 | 83 | jest.mock( 84 | '../../../vscode-extension/commands/ui-commands/webviewHandler', 85 | () => ({ 86 | ContrastPanelInstance: { 87 | postMessage: jest.fn(), 88 | }, 89 | }) 90 | ); 91 | 92 | const mockVulnerabilityData = { 93 | responseData: { 94 | child: [ 95 | { 96 | label: 'file1.js', 97 | child: [ 98 | { 99 | traceId: 'trace123', 100 | vulnerabilityDetails: 'vulnerability 1 details', 101 | }, 102 | { 103 | traceId: 'trace456', 104 | vulnerabilityDetails: 'vulnerability 2 details', 105 | }, 106 | ], 107 | }, 108 | { 109 | label: 'file2.js', 110 | child: [ 111 | { 112 | traceId: 'trace789', 113 | vulnerabilityDetails: 'vulnerability 3 details', 114 | }, 115 | ], 116 | }, 117 | ], 118 | }, 119 | }; 120 | 121 | describe('getVulnerabilityByTraceId', () => { 122 | const traceId = 'trace123'; 123 | 124 | beforeEach(() => { 125 | jest.clearAllMocks(); 126 | }); 127 | 128 | it('should return the vulnerability details when traceId is found', async () => { 129 | (getDataOnlyFromCacheAssess as jest.Mock).mockResolvedValue( 130 | mockVulnerabilityData 131 | ); 132 | (localeI18ln.getTranslation as jest.Mock).mockReturnValue( 133 | 'Vulnerability fetched successfully' 134 | ); 135 | (resolveSuccess as jest.Mock).mockReturnValue({ 136 | message: 'Success', 137 | statusCode: 200, 138 | data: mockVulnerabilityData.responseData.child[0].child[0], 139 | }); 140 | 141 | const result = await getVulnerabilityByTraceId(traceId); 142 | 143 | expect(getDataOnlyFromCacheAssess).toHaveBeenCalled(); 144 | expect(resolveSuccess).toHaveBeenCalledWith( 145 | 'Vulnerability fetched successfully', 146 | 200, 147 | mockVulnerabilityData.responseData.child[0].child[0] 148 | ); 149 | expect(result).toEqual({ 150 | message: 'Success', 151 | statusCode: 200, 152 | data: mockVulnerabilityData.responseData.child[0].child[0], 153 | }); 154 | }); 155 | 156 | it('should return undefined if the traceId is not found', async () => { 157 | (getDataOnlyFromCacheAssess as jest.Mock).mockResolvedValue( 158 | mockVulnerabilityData 159 | ); 160 | (localeI18ln.getTranslation as jest.Mock).mockReturnValue( 161 | 'TraceId not found' 162 | ); 163 | (resolveFailure as jest.Mock).mockReturnValue({ 164 | message: 'TraceId not found', 165 | statusCode: 400, 166 | }); 167 | 168 | const result = await getVulnerabilityByTraceId('trace999'); 169 | 170 | expect(getDataOnlyFromCacheAssess).toHaveBeenCalled(); 171 | expect(result).toBeUndefined(); 172 | expect(resolveFailure).not.toHaveBeenCalled(); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /src/webview/screens/Assess/tabs/LibraryReport/tabs/LibraryPath.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | ContrastAssessLocale, 4 | PassLocalLang, 5 | ReducerTypes, 6 | } from '../../../../../../common/types'; 7 | import { 8 | LibParsedVulnerability, 9 | LibraryNode, 10 | } from '../../../../../../vscode-extension/api/model/api.interface'; 11 | import { useSelector } from 'react-redux'; 12 | import { 13 | getLibraryNodeByUuid, 14 | scaPathUpdate, 15 | } from '../../../../../utils/helper'; 16 | import FolderIcon from '@mui/icons-material/Folder'; 17 | import { webviewPostMessage } from '../../../../../utils/postMessage'; 18 | import { 19 | WEBVIEW_COMMANDS, 20 | WEBVIEW_SCREENS, 21 | } from '../../../../../../vscode-extension/utils/constants/commands'; 22 | 23 | export function LibraryPath({ 24 | vulnerability, 25 | translate, 26 | }: { 27 | translate: PassLocalLang; 28 | vulnerability: unknown; 29 | }) { 30 | const [libraryPaths, setLibraryPaths] = useState< 31 | { path: string; link: string }[] 32 | >([]); 33 | const [isPathAvailable, setIsPathAvailable] = useState(false); 34 | const [selectedLibraryNode, setSelectedLibraryNode] = 35 | useState(null); 36 | 37 | const scaAllFilesData = useSelector( 38 | (state: ReducerTypes) => state.assessFilter.scaAllFiles 39 | ); 40 | const scaAutoRefresh = useSelector( 41 | (state: ReducerTypes) => state.assessFilter.scaAutoRefresh 42 | ); 43 | 44 | const [properties, setProperties] = useState<{ 45 | noDataFoundLable: string; 46 | noDataFoundContent: string; 47 | }>({ 48 | noDataFoundLable: 'No Path found', 49 | noDataFoundContent: 50 | 'Note: Please check the corresponding manifest files for the selected library.', 51 | }); 52 | 53 | useEffect(() => { 54 | if (translate !== null && translate !== undefined) { 55 | const locale = translate as unknown as ContrastAssessLocale; 56 | const pathData = locale?.librariesReport?.tabs?.path; 57 | setProperties({ 58 | noDataFoundLable: pathData?.noDataFoundLable ?? 'No Path found', 59 | noDataFoundContent: 60 | pathData?.noDataFoundContent ?? 61 | 'Note: Please check the corresponding manifest files for the selected library.', 62 | }); 63 | } 64 | }, [translate]); 65 | 66 | useEffect(() => { 67 | if (vulnerability !== null && vulnerability !== undefined) { 68 | const node = vulnerability as unknown as LibraryNode; 69 | if (node.level === 1) { 70 | setSelectedLibraryNode(node); 71 | setIsPathAvailable(node?.path !== null && node?.path.length > 0); 72 | setLibraryPaths(node?.path); 73 | } 74 | } else { 75 | setIsPathAvailable(false); 76 | } 77 | }, [vulnerability]); 78 | 79 | useEffect(() => { 80 | if ( 81 | scaAllFilesData !== undefined && 82 | scaAllFilesData !== null && 83 | scaAllFilesData.responseData !== undefined && 84 | scaAllFilesData.responseData !== null 85 | ) { 86 | const parsedVulnerability = 87 | scaAllFilesData.responseData as LibParsedVulnerability; 88 | 89 | if ( 90 | !isPathAvailable && 91 | selectedLibraryNode !== null && 92 | selectedLibraryNode !== undefined 93 | ) { 94 | const resolvedNode = getLibraryNodeByUuid( 95 | parsedVulnerability, 96 | selectedLibraryNode?.overview?.hash, 97 | selectedLibraryNode?.isUnmapped 98 | ); 99 | 100 | if (resolvedNode !== undefined && resolvedNode !== null) { 101 | const resolvedPaths = resolvedNode?.path ?? []; 102 | setLibraryPaths(resolvedPaths); 103 | } 104 | } 105 | } 106 | }, [scaAllFilesData, isPathAvailable, selectedLibraryNode]); 107 | 108 | useEffect(() => { 109 | if ( 110 | !isPathAvailable && 111 | vulnerability !== null && 112 | vulnerability !== undefined && 113 | scaAutoRefresh !== null 114 | ) { 115 | const node = vulnerability as unknown as LibraryNode; 116 | scaPathUpdate(node); 117 | } 118 | }, [isPathAvailable, scaAutoRefresh]); 119 | 120 | return ( 121 |
    122 | {libraryPaths.length > 0 ? ( 123 | 144 | ) : ( 145 |
    146 |
    147 | {properties.noDataFoundLable} 148 |
    149 |
    150 | {properties.noDataFoundContent} 151 |
    152 |
    153 | )} 154 |
    155 | ); 156 | } 157 | -------------------------------------------------------------------------------- /src/vscode-extension/utils/constants/commands.ts: -------------------------------------------------------------------------------- 1 | export const CONSTRAST_SETTING = 'contrast.setting'; 2 | export const CONSTRAST_ABOUT = 'contrast.about'; 3 | export const CONSTRAST_SCAN = 'contrast.scan'; 4 | export const CONSTRAST_ASSESS = 'contrast.assess'; 5 | 6 | export const CONSTRAST_PANEL: string = 'Contrast.Panel'; 7 | export const CONSTRAST_ACTIVITYBAR = 'Contrast.activityBar'; 8 | 9 | export const CONSTRAST_REPORT_VULNERABILITIES_OPEN = 10 | 'contrast.report.vulnerability.open'; 11 | export const CONTRAST_RETRIEVE_VULNERABILITIES = 'contrast.retrieveVul'; 12 | export const CONTRAST_STATUSBAR_CLICK = 'contrast.statusBarOnClick'; 13 | 14 | export const TAB_BLOCKER = 'contrast.tab.blocker'; 15 | 16 | // Configuration Commands 17 | export const CONTRAST_SECURITY = 'contrastSecurity'; 18 | export const CONTRAST_SECURITY_GLOBAL_SHARING = 'globalSharing'; 19 | 20 | //Theme Commands 21 | export const CONTRAST_THEME = 'contrastTheme'; 22 | 23 | //Webview Commands 24 | 25 | export enum WEBVIEW_SCREENS { 26 | SETTING = 'CONFIGURE_SETTING', 27 | SCAN = 'CONFIGURE_SCAN', 28 | ASSESS = 'CONFIGURE_ASSESS', 29 | } 30 | 31 | export enum WEBVIEW_COMMANDS { 32 | // Setting 33 | SETTING_ADD_PROJECT_TO_CONFIGURE = 'addProjectToConfig', 34 | SETTING_GET_CONFIGURE_PROJECTS = 'getOrgProjects', 35 | SETTING_GET_ALL_PROJECTS = 'getAllProjects', 36 | SETTING_GET_ALL_APPLICATIONS = 'getAllApplication', // New 37 | SETTING_UPDATE_CONFIGURE_PROJECT = 'updateOrgProject', 38 | SETTING_DELETE_CONFIGURE_PROJECT = 'deleteOrgProject', 39 | SETTING_CANCEL_STATE_WHILE_DELETE = 'cancelStateWhileDelete', 40 | SETTING_ACTIONS = 'settingActions', 41 | SCAN_OPEN_VULNERABILITY_FILE = 'openVulnerabilityFile', 42 | SCAN_GET_CURRENTFILE_VUL = 'getCurrentFileVul', 43 | SCAN_GET_ALL_FILES_VULNERABILITY = 'getAllFilesVulnerability', 44 | SCAN_RETRIEVEL_DETECT_ACROSS_IDS = 'scanRetrievelDetectAcrossIds', 45 | 46 | // Scan 47 | SCAN_ACTIVE_PROJECT_NAME = 'activeProjectName', 48 | SCAN_VALID_CONFIGURED_PROJECTS = 'validConfiguredProjects', 49 | SCAN_BACKGROUND_RUNNER = 'scanBackgroundRunner', 50 | SCAN_MANUAL_REFRESH_BACKGROUND_RUNNER = 'scanManulaRefreshBackgroundRunner', 51 | SCAN_UPDATE_FILTERS = 'updateFilters', 52 | SCAN_GET_FILTERS = 'getFilters', 53 | SCAN_MANUAL_REFRESH = 'manualRefresh', 54 | 55 | // Assess 56 | GET_CONFIGURED_APPLICATIONS = 'getConfiguredApplications', 57 | GET_SERVER_LIST_BY_ORG_ID = 'getServerListbyOrgId', 58 | GET_BUILD_NUMBER = 'getBuildNumber', 59 | GET_ASSESS_ENVIRONMENTS = 'getAssessEnvironmets', 60 | GET_ASSESS_TAGS = 'getAssessTags', 61 | GET_CUSTOM_SESSION_METADATA = 'getCustomSessionMetaData', 62 | GET_MOST_RECENT_METADATA = 'getMostRecentMetaData', 63 | COMMON_MESSAGE = 'commonMessage', 64 | ASSESS_UPDATE_FILTERS = 'assessUpdateFilters', 65 | ASSESS_GET_FILTERS = 'assessGetFilters', 66 | ASSESS_GET_ALL_FILES_VULNERABILITY = 'assessGetAllFilesVulnerability', 67 | ASSESS_GET_INITIAL_ALL_FILES_VULNERABILITY = 'assessGetInitialsAllFilesVulnerability', 68 | ASSESS_BACKGROUND_RUNNER = 'assessBackgroundRunner', 69 | ASSESS_REDIRECTION = 'assessRedirection', 70 | ASSESS_UPDATE_VULNERABILITY = 'assessUpdateVulnerability', 71 | ASSESS_MANUAL_REFRESH = 'assessManualRefresh', 72 | ASSESS_ADD_MARK = 'assessAddMark', 73 | ASSESS_ORG_TAGS = 'assessOrganizationTags', 74 | ASSESS_TAG_ALREADY_APPLIED = 'tagAlreadyApplied', 75 | ASSESS_TAG_ALREADY_AVAILABLE = 'tagAlreadyAvailable', 76 | ASSESS_VULNERABILITY_TAGGED = 'vulnerabilityTagged', 77 | ASSESS_TAG_LENGTH_EXCEEDED = 'tagLengthExceeded', 78 | ASSESS_TAG_OK_BEHAVIOUR = 'tagOkBehaviour', 79 | ASSESS_GET_CURRENTFILE_VUL = 'getAssessCurrentFileVul', 80 | ASSESS_OPEN_VULNERABILITY_FILE = 'openAssessVulnerabilityFile', 81 | ASSESS_MANUAL_REFRESH_BACKGROUND_RUNNER = 'assessManulaRefreshBackgroundRunner', 82 | ASSESS_REFRESH_BACKGROUND_RUNNER_ACROSS_IDS = 'assessRefreshBackgroundRunnerAcrossIds', 83 | 84 | // SCA 85 | SCA_ENVIRONMENTS_LIST = 'scaEnvironmentsList', 86 | SCA_SERVERS_LIST = 'scaServersList', 87 | SCA_QUICKVIEW_LIST = 'scaQuickViewist', 88 | SCA_LIBRARY_USAGE_LIST = 'scaLibraryUsageList', 89 | SCA_LIBRARY_LICENSES_LIST = 'scaLibraryLicensesList', 90 | SCA_TAG_LIST = 'scaTagList', 91 | SCA_GET_FILTERS = 'scaGetFilters', 92 | SCA_UPDATE_FILTERS = 'scaUpdateFilters', 93 | SCA_SEVERITIES = 'scaSeverities', 94 | SCA_STATUS = 'scaStatus', 95 | SCA_GET_ALL_FILES_VULNERABILITY = 'scaGetAllFilesVulnerability', 96 | SCA_UPDATE_VULNERABILITY_USAGE = 'scaUpdateVulnerabilityUsage', 97 | SCA_ORG_TAGS = 'scaOrganizationTags', 98 | SCA_TAG_OK_BEHAVIOUR = 'scaTagOkBehaviour', 99 | SCA_UPDATE_CVE_OVERVIEW = 'scaUpdateCveOverview', 100 | SCA_UPDATE_CVE_PATH = 'scaUpdateCvePath', 101 | SCA_LIBRARY_PATH_REDIRECT = 'scaLibraryPathRedirect', 102 | SCA_AUTO_REFRESH = 'scaAutoRefresh', 103 | SCA_GET_INITIAL_ALL_FILES_VULNERABILITY = 'scaGetInitialsAllFilesVulnerability', 104 | } 105 | 106 | export enum EXTENTION_COMMANDS { 107 | SETTING_SCREEN = 1, 108 | SCAN_SCREEN = 2, 109 | ASSESS_SCREEN = 3, 110 | L10N = 'i10n', 111 | CURRENT_FILE = 'current_file', 112 | VULNERABILITY_REPORT = 'vulnerability_report', 113 | ASSESS_CURRENT_FILE = 'assess_current_file', 114 | } 115 | 116 | export enum TOKEN { 117 | SETTING = 'SETTING', 118 | SCAN = 'SCAN', 119 | ASSESS = 'ASSESS', 120 | } 121 | 122 | export enum SETTING_KEYS { 123 | CONFIGPROJECT = 'configuredProjects', 124 | } 125 | 126 | export enum SCAN_KEYS { 127 | FILTERS = 'scaFilters', 128 | } 129 | 130 | export enum ASSESS_KEYS { 131 | ASSESS_FILTERS = 'assessFilters', 132 | SCA_FILTERS = 'scaFilters', 133 | } 134 | --------------------------------------------------------------------------------