(
38 | 'No vulnerabilities found:
- Navigate to the Contrast - Assess menu
- Select the necessary filters and click Run Button.
- 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 |
70 |
71 |
72 |
73 | | Plugin Name |
74 | ${displayName} |
75 |
76 |
77 |
78 | | Plugin Release Version |
79 | ${version} |
80 |
81 |
82 |
83 | | IDE Version |
84 | ${IDEVersion} |
85 |
86 |
87 | | OS Version |
88 | ${osWithVersion} |
89 |
90 |
91 |
92 | | Platform |
93 | ${platform} |
94 |
95 |
96 |
97 |
98 |
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 |
--------------------------------------------------------------------------------