diff --git a/media/icons/cxone-assist-cube.svg b/media/icons/cxone-assist-cube.svg new file mode 100644 index 00000000..be47b001 --- /dev/null +++ b/media/icons/cxone-assist-cube.svg @@ -0,0 +1,3182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + +
+ + + + + + + + + + + + + +
+ + + + + + +
+
+ + + + + + + +
+ + + + + + +
+
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + +
+ + + + + + +
+
+ + + + + + +
+ + + + + + +
+ + + + + + + +
+ + + + + + +
+
+ + + + +
+
+ + + + + +
+ + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/media/riskManagement.js b/media/riskManagement.js index 81dd7d10..e98723c3 100644 --- a/media/riskManagement.js +++ b/media/riskManagement.js @@ -190,17 +190,21 @@ break; } case "showLoader": { - document.getElementById("loading").classList.remove("hidden"); - document - .getElementById("riskManagementContainer") - .classList.add("hidden"); + const loadingEl = document.getElementById("loading"); + const containerEl = document.getElementById("riskManagementContainer"); + loadingEl && loadingEl.classList.remove("hidden"); + if (containerEl) { + containerEl.classList.add("hidden"); + } break; } case "hideLoader": { - document.getElementById("loading").classList.add("hidden"); - document - .getElementById("riskManagementContainer") - .classList.remove("hidden"); + const loadingEl = document.getElementById("loading"); + const containerEl = document.getElementById("riskManagementContainer"); + loadingEl && loadingEl.classList.add("hidden"); + if (containerEl) { + containerEl.classList.remove("hidden"); + } break; } } @@ -495,7 +499,7 @@ function extractTraits(results) { const traitsSet = new Set(); - results.results.forEach((r) => { + results?.results.forEach((r) => { if (r.traits && typeof r.traits === "object") { Object.values(r.traits).forEach((t) => traitsSet.add(t)); } @@ -513,6 +517,9 @@ return; } + if(!submenu || !category) { + return; + } submenu.classList.add("hidden"); category.classList.remove("hidden"); category.classList.remove("expanded"); diff --git a/media/vscode.css b/media/vscode.css index 2255ea83..4f7edef6 100644 --- a/media/vscode.css +++ b/media/vscode.css @@ -12,7 +12,7 @@ body { font-size: var(--vscode-font-size); font-weight: var(--vscode-font-weight); font-family: var(--vscode-font-family); - background-color: var(--vscode-editor-background) !important; + background-color: var(--vscode-background) !important; } ol, diff --git a/package-lock.json b/package-lock.json index b7133585..5727556b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "ast-results", "version": "2.42.0", "dependencies": { - "@checkmarxdev/ast-cli-javascript-wrapper": "0.0.146", + "@checkmarxdev/ast-cli-javascript-wrapper": "v0.0.147-rc-standalone-changes.0", "@popperjs/core": "^2.11.8", "@vscode/codicons": "^0.0.36", "axios": "1.12.2", @@ -527,9 +527,9 @@ }, "node_modules/@checkmarxdev/ast-cli-javascript-wrapper": { "name": "@CheckmarxDev/ast-cli-javascript-wrapper", - "version": "0.0.146", - "resolved": "https://npm.pkg.github.com/download/@CheckmarxDev/ast-cli-javascript-wrapper/0.0.146/ca37569f208a7edf77e46e5a351ddce7e5ac760a", - "integrity": "sha512-p0jj/UeJqJysCyd3kuZLKqltKQuWjV7AyVup6ivyi5ew2ssJX5mS75SgKBtWBnHChWqhbxnQ4OipuzKKs1Sznw==", + "version": "0.0.147-rc-standalone-changes.0", + "resolved": "https://npm.pkg.github.com/download/@CheckmarxDev/ast-cli-javascript-wrapper/0.0.147-rc-standalone-changes.0/2a9c1e2a9f66ebe389f5c7edc14d2d0f799d73fe", + "integrity": "sha512-98/bWrVw7dEcDxKFs8I7e+q83kLpGD4tsmRK/cdmzkWwLDroI/Zl/h0nGataIOj+TKDY46xv//RBUmSw+wF8YA==", "license": "ISC", "dependencies": { "log4js": "^6.9.1" diff --git a/package.json b/package.json index cb973075..d4e8fdfb 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,17 @@ "title": "Clear Results Selection", "enablement": "ast-results.isValidCredentials" }, + { + "command": "ast-results.openIgnoredView", + "category": "ast-results", + "title": "View Ignored vulnerabilities", + "enablement": "ast-results.isValidCredentials && ast-results.isCxOneAssistEnabled" + }, + { + "command": "ast-results.assistDocumentation", + "category": "ast-results", + "title": "Documentation" + }, { "command": "ast-results.groupBySeverityActive", "category": "ast-results", @@ -810,10 +821,20 @@ "group": "@groupBy@7", "when": "view == astResults && ast-results-groupByDirectDependency" }, + { + "command": "ast-results.openIgnoredView", + "group": "@settings@1", + "when": "view == astCxOneAssist" + }, { "command": "ast-results.viewSettings", "group": "@settings@2", - "when": "view == astResults || view==scaAutoScan" + "when": "view == astResults || view==scaAutoScan || view==astCxOneAssist || view==astResultsPromo || view==scaAutoScanPromo || view==riskManagement" + }, + { + "command": "ast-results.assistDocumentation", + "group": "@settings@3", + "when": "view == astCxOneAssist" }, { "command": "ast-results.clear", @@ -842,12 +863,31 @@ { "id": "astResults", "type": "tree", - "name": "Checkmarx One Results" + "name": "Checkmarx One Results", + "when": "!ast-results.isStandaloneEnabled" + }, + { + "id": "astResultsPromo", + "type": "webview", + "name": "Checkmarx One Results", + "when": "ast-results.isStandaloneEnabled" + }, + { + "id": "astCxOneAssist", + "type": "webview", + "name": "Checkmarx One Assist" }, { "id": "scaAutoScan", "type": "tree", - "name": "Checkmarx SCA Realtime Scanner" + "name": "Checkmarx SCA Realtime Scanner", + "when": "!ast-results.isStandaloneEnabled" + }, + { + "id": "scaAutoScanPromo", + "type": "webview", + "name": "Checkmarx SCA Realtime Scanner", + "when": "ast-results.isStandaloneEnabled" }, { "id": "riskManagement", @@ -1059,7 +1099,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "@checkmarxdev/ast-cli-javascript-wrapper": "0.0.146", + "@checkmarxdev/ast-cli-javascript-wrapper": "v0.0.147-rc-standalone-changes.0", "@popperjs/core": "^2.11.8", "@vscode/codicons": "^0.0.36", "axios": "1.12.2", @@ -1095,4 +1135,4 @@ "pre-commit": "lint-staged" } } -} +} \ No newline at end of file diff --git a/src/commands/commonCommand.ts b/src/commands/commonCommand.ts index 91c17f29..3e8e2ba7 100644 --- a/src/commands/commonCommand.ts +++ b/src/commands/commonCommand.ts @@ -40,7 +40,7 @@ export class CommonCommand { public async executeCheckSettings() { - const isConfiguration = await cx.isValidConfiguration(); + const isConfiguration = await cx.isValidConfiguration(); vscode.commands.executeCommand( commands.setContext, commands.isValidCredentials, @@ -52,7 +52,7 @@ export class CommonCommand { commands.isScaScanEnabled, true); - this.executeCheckScanEnabled(); + this.executeCheckScanEnabled(); } public async executeCheckScanEnabled() { @@ -63,6 +63,22 @@ export class CommonCommand { ); } + public async executeCheckStandaloneEnabled() { + vscode.commands.executeCommand( + commands.setContext, + commands.isStandaloneEnabled, + await cx.isStandaloneEnabled(this.logs) + ); + } + + public async executeCheckCxOneAssistEnabled() { + vscode.commands.executeCommand( + commands.setContext, + commands.isCxOneAssistEnabled, + await cx.isCxOneAssistEnabled(this.logs) + ); + } + public async executeCheckScaScanEnabled() { vscode.commands.executeCommand( commands.setContext, diff --git a/src/commands/kicsRealtimeCommand.ts b/src/commands/kicsRealtimeCommand.ts index d037adb5..1bd32b4a 100644 --- a/src/commands/kicsRealtimeCommand.ts +++ b/src/commands/kicsRealtimeCommand.ts @@ -6,6 +6,7 @@ import { } from "../utils/common/commands"; import { constants } from "../utils/common/constants"; import { messages } from "../utils/common/messages"; +import { cx } from "../cx"; export class KICSRealtimeCommand { context: vscode.ExtensionContext; @@ -48,6 +49,9 @@ export class KICSRealtimeCommand { fixAll, fixLine ) => { + if (await cx.isStandaloneEnabled(this.logs)) { + return; + } await this.kicsProvider.kicsRemediation( fixedResults, kicsResults, @@ -66,7 +70,12 @@ export class KICSRealtimeCommand { this.context.subscriptions.push( vscode.commands.registerCommand( commands.kicsRealtime, - async () => await this.kicsProvider.runKicsIfEnabled() + async () => { + if (await cx.isStandaloneEnabled(this.logs)) { + return; + } + await this.kicsProvider.runKicsIfEnabled(); + } ) ); } diff --git a/src/commands/scanSCACommand.ts b/src/commands/scanSCACommand.ts index 5046ec90..267122a5 100644 --- a/src/commands/scanSCACommand.ts +++ b/src/commands/scanSCACommand.ts @@ -3,6 +3,7 @@ import { Logs } from "../models/logs"; import { commands } from "../utils/common/commands"; import { createSCAScan } from "../views/scaView/scaCreateScanProvider"; import { SCAResultsProvider } from "../views/scaView/scaResultsProvider"; +import { cx } from "../cx"; export class ScanSCACommand { context: vscode.ExtensionContext; @@ -21,19 +22,19 @@ export class ScanSCACommand { this.logs = logs; } - public registerScaScans() { + public async registerScaScans() { this.createScanCommand(); } private createScanCommand() { this.context.subscriptions.push( vscode.commands.registerCommand(commands.createScaScan, async () => { - await createSCAScan( + !await cx.isStandaloneEnabled(this.logs) ? await createSCAScan( this.context, this.runSCAScanStatusBar, this.logs, this.scaResultsProvider - ); + ) : undefined; }) ); } diff --git a/src/constants/documentation.ts b/src/constants/documentation.ts new file mode 100644 index 00000000..74422338 --- /dev/null +++ b/src/constants/documentation.ts @@ -0,0 +1,3 @@ +export const DOC_LINKS = { + devAssist: "https://docs.checkmarx.com/en/34965-405960-checkmarx-one-developer-assist.html", +}; diff --git a/src/cx/cx.ts b/src/cx/cx.ts index 2512d631..341276d2 100644 --- a/src/cx/cx.ts +++ b/src/cx/cx.ts @@ -307,7 +307,7 @@ export class Cx implements CxPlatform { } async getAstConfiguration() { - const token = await this.context.secrets.get("authCredential"); + const token = await this.context.secrets.get(constants.authCredentialSecretKey); if (!token) { return undefined; @@ -319,7 +319,7 @@ export class Cx implements CxPlatform { } async isValidConfiguration(): Promise { - const token = await this.context.secrets.get("authCredential"); + const token = await this.context.secrets.get(constants.authCredentialSecretKey); if (!token) { return false; @@ -337,7 +337,7 @@ export class Cx implements CxPlatform { async isScanEnabled(logs: Logs): Promise { let enabled = false; - const token = await this.context.secrets.get("authCredential"); + const token = await this.context.secrets.get(constants.authCredentialSecretKey); if (!token) { return enabled; } @@ -359,7 +359,7 @@ export class Cx implements CxPlatform { async isAIGuidedRemediationEnabled(logs: Logs): Promise { let enabled = true; - const token = await this.context.secrets.get("authCredential"); + const token = await this.context.secrets.get(constants.authCredentialSecretKey); if (!token) { return enabled; } @@ -378,9 +378,91 @@ export class Cx implements CxPlatform { return enabled; } + async isStandaloneEnabled(logs: Logs): Promise { + return this.getCachedFeatureEnabled( + constants.standaloneEnabledGlobalState, + logs, + async (cx: CxWrapper) => cx.standaloneEnabled(), + "tenant configuration" + ); + } + + + async isCxOneAssistEnabled(logs: Logs): Promise { + return this.getCachedFeatureEnabled( + constants.cxOneAssistEnabledGlobalState, + logs, + async (cx: CxWrapper) => { + const anyCx = cx as unknown as { cxOneAssistEnabled?: () => Promise }; + return anyCx.cxOneAssistEnabled ? await anyCx.cxOneAssistEnabled() : false; + }, + "tenant configuration (CxOne Assist)" + ); + } + + async refreshStandaloneEnabled(logs: Logs): Promise { + await this.context.globalState.update(constants.standaloneEnabledGlobalState, undefined); + return this.isStandaloneEnabled(logs); + } + + clearStandaloneEnabledCache(): void { + this.context.globalState.update(constants.standaloneEnabledGlobalState, undefined); + } + + private async setStandaloneFlag(value: boolean): Promise { + await this.context.globalState.update(constants.standaloneEnabledGlobalState, value); + } + + private async clearStandaloneFlag(): Promise { + await this.context.globalState.update(constants.standaloneEnabledGlobalState, undefined); + } + + private async getCachedFeatureEnabled( + globalStateKey: string, + logs: Logs, + remoteCheck: (cx: CxWrapper) => Promise, + errorContext: string + ): Promise { + const token = await this.context.secrets.get(constants.authCredentialSecretKey); + if (!token) { + await this.context.globalState.update(globalStateKey, undefined); + return false; + } + + const cached = this.context.globalState.get(globalStateKey); + if (cached !== undefined) { + return cached; + } + + const config = await this.getAstConfiguration(); + if (!config) { + await this.context.globalState.update(globalStateKey, false); + return false; + } + + const cx = new CxWrapper(config); + try { + const enabled = await remoteCheck(cx); + if (globalStateKey === constants.standaloneEnabledGlobalState) { + await this.setStandaloneFlag(enabled); + } else { + await this.context.globalState.update(globalStateKey, enabled); + } + return enabled; + } catch (error) { + logs.error(`Error checking ${errorContext}: ${error}`); + if (globalStateKey === constants.standaloneEnabledGlobalState) { + await this.setStandaloneFlag(false); + } else { + await this.context.globalState.update(globalStateKey, false); + } + return false; + } + } + async isAiMcpServerEnabled(): Promise { let enabled = false; - const token = await this.context.secrets.get("authCredential"); + const token = await this.context.secrets.get(constants.authCredentialSecretKey); if (!token) { return enabled; diff --git a/src/cx/cxMock.ts b/src/cx/cxMock.ts index 43d0d17d..4e49bdc7 100644 --- a/src/cx/cxMock.ts +++ b/src/cx/cxMock.ts @@ -35,7 +35,7 @@ export class CxMock implements CxPlatform { label: "sca", severity: "HIGH", description: - "decode-uri-component is vulnerable to Improper Input Validation resulting in DoS.", + "decode-uri-component is vulnerable to Improper Input Validation resulting in DoS.", data: { nodes: [ { @@ -121,7 +121,7 @@ export class CxMock implements CxPlatform { label: "IaC Security", id: "150256", similarityId: - "92742543fa8505b3ba24ff54894c94594d0d9e3873b05fb38dbcbdef40aa3062", + "92742543fa8505b3ba24ff54894c94594d0d9e3873b05fb38dbcbdef40aa3062", status: "NEW", state: "TO_VERIFY", severity: "LOW", @@ -130,12 +130,12 @@ export class CxMock implements CxPlatform { foundAt: "2022-09-02T10:45:29Z", firstScanId: "cb834dcd-aaec-4e1f-a099-089d5fbe503e", description: - "Ensure that HEALTHCHECK is being used. The HEALTHCHECK instruction tells Docker how to test a container to check that it is still working", + "Ensure that HEALTHCHECK is being used. The HEALTHCHECK instruction tells Docker how to test a container to check that it is still working", descriptionHTML: - "\u003cp\u003eEnsure that HEALTHCHECK is being used. The HEALTHCHECK instruction tells Docker how to test a container to check that it is still working\u003c/p\u003e\n", + "\u003cp\u003eEnsure that HEALTHCHECK is being used. The HEALTHCHECK instruction tells Docker how to test a container to check that it is still working\u003c/p\u003e\n", data: { queryId: - "b03a748a-542d-44f4-bb86-9199ab4fd2d5 [Taken from query_id]", + "b03a748a-542d-44f4-bb86-9199ab4fd2d5 [Taken from query_id]", queryName: "Healthcheck Instruction Missing", group: "Insecure Configurations [Taken from category]", line: 3, @@ -155,7 +155,7 @@ export class CxMock implements CxPlatform { label: "IaC Security", id: "150255", similarityId: - "bca11aa6fe8840e47585aaa38048073afc521dc953151be020fb8fc4cc38f54a", + "bca11aa6fe8840e47585aaa38048073afc521dc953151be020fb8fc4cc38f54a", status: "NEW", state: "TO_VERIFY", severity: "MEDIUM", @@ -164,21 +164,21 @@ export class CxMock implements CxPlatform { foundAt: "2022-09-02T10:45:29Z", firstScanId: "cb834dcd-aaec-4e1f-a099-089d5fbe503e", description: - "Package version pinning reduces the range of versions that can be installed, reducing the chances of failure due to unanticipated changes", + "Package version pinning reduces the range of versions that can be installed, reducing the chances of failure due to unanticipated changes", descriptionHTML: - "\u003cp\u003ePackage version pinning reduces the range of versions that can be installed, reducing the chances of failure due to unanticipated changes\u003c/p\u003e\n", + "\u003cp\u003ePackage version pinning reduces the range of versions that can be installed, reducing the chances of failure due to unanticipated changes\u003c/p\u003e\n", data: { queryId: - "d3499f6d-1651-41bb-a9a7-de925fea487b [Taken from query_id]", + "d3499f6d-1651-41bb-a9a7-de925fea487b [Taken from query_id]", queryName: "Unpinned Package Version in Apk Add", group: "Supply-Chain [Taken from category]", line: 6, platform: "Dockerfile", issueType: "IncorrectValue", expectedValue: - "RUN instruction with 'apk add \u003cpackage\u003e' should use package pinning form 'apk add \u003cpackage\u003e=\u003cversion\u003e'", + "RUN instruction with 'apk add \u003cpackage\u003e' should use package pinning form 'apk add \u003cpackage\u003e=\u003cversion\u003e'", value: - "RUN instruction apk --no-cache add git python3 py-lxml \u0026\u0026 rm -rf /var/cache/apk/* does not use package pinning form", + "RUN instruction apk --no-cache add git python3 py-lxml \u0026\u0026 rm -rf /var/cache/apk/* does not use package pinning form", filename: "/Dockerfile", }, comments: {}, @@ -199,9 +199,9 @@ export class CxMock implements CxPlatform { foundAt: "2022-09-02T10:45:54Z", firstScanId: "cb834dcd-aaec-4e1f-a099-089d5fbe503e", description: - "The method $PageLoad embeds untrusted data in generated output with echo, at line 11 of /insecure.php. This untrusted data is embedded into the output without proper sanitization or encoding, enabling an attacker to inject malicious code into the generated web-page.\n\nThe attacker would be able to alter the returned web page by simply providing modified data in the user input _POST, which is read by the $PageLoad method at line 10 of /insecure.php. This input then flows through the code straight to the output web page, without sanitization. \r\n\r\nThis can enable a Reflected Cross-Site Scripting (XSS) attack.\n\n", + "The method $PageLoad embeds untrusted data in generated output with echo, at line 11 of /insecure.php. This untrusted data is embedded into the output without proper sanitization or encoding, enabling an attacker to inject malicious code into the generated web-page.\n\nThe attacker would be able to alter the returned web page by simply providing modified data in the user input _POST, which is read by the $PageLoad method at line 10 of /insecure.php. This input then flows through the code straight to the output web page, without sanitization. \r\n\r\nThis can enable a Reflected Cross-Site Scripting (XSS) attack.\n\n", descriptionHTML: - "\u003cp\u003eThe method $PageLoad embeds untrusted data in generated output with echo, at line 11 of /insecure.php. This untrusted data is embedded into the output without proper sanitization or encoding, enabling an attacker to inject malicious code into the generated web-page.\u003c/p\u003e\n\n\u003cp\u003eThe attacker would be able to alter the returned web page by simply providing modified data in the user input _POST, which is read by the $PageLoad method at line 10 of /insecure.php. This input then flows through the code straight to the output web page, without sanitization. \r\n\r\nThis can enable a Reflected Cross-Site Scripting (XSS) attack.\u003c/p\u003e\n", + "\u003cp\u003eThe method $PageLoad embeds untrusted data in generated output with echo, at line 11 of /insecure.php. This untrusted data is embedded into the output without proper sanitization or encoding, enabling an attacker to inject malicious code into the generated web-page.\u003c/p\u003e\n\n\u003cp\u003eThe attacker would be able to alter the returned web page by simply providing modified data in the user input _POST, which is read by the $PageLoad method at line 10 of /insecure.php. This input then flows through the code straight to the output web page, without sanitization. \r\n\r\nThis can enable a Reflected Cross-Site Scripting (XSS) attack.\u003c/p\u003e\n", data: { // eslint-disable-next-line @typescript-eslint/no-loss-of-precision queryId: 5157925289005576664, @@ -221,7 +221,7 @@ export class CxMock implements CxPlatform { domType: "UnknownReference", fileName: "/insecure.php", fullName: - "$NS_insecure_a3ce4a23.$Cls_insecure_a3ce4a23._POST", + "$NS_insecure_a3ce4a23.$Cls_insecure_a3ce4a23._POST", typeName: "CxDefaultObject", methodLine: 1, definitions: "1", @@ -316,9 +316,9 @@ export class CxMock implements CxPlatform { foundAt: "2022-09-02T10:45:54Z", firstScanId: "cb834dcd-aaec-4e1f-a099-089d5fbe503e", description: - "The method $PageLoad embeds untrusted data in generated output with echo, at line 11 of /insecure.php. This untrusted data is embedded into the output without proper sanitization or encoding, enabling an attacker to inject malicious code into the generated web-page.\n\nThe attacker would be able to alter the returned web page by simply providing modified data in the user input _POST, which is read by the $PageLoad method at line 10 of /insecure.php. This input then flows through the code straight to the output web page, without sanitization. \r\n\r\nThis can enable a Reflected Cross-Site Scripting (XSS) attack.\n\n", + "The method $PageLoad embeds untrusted data in generated output with echo, at line 11 of /insecure.php. This untrusted data is embedded into the output without proper sanitization or encoding, enabling an attacker to inject malicious code into the generated web-page.\n\nThe attacker would be able to alter the returned web page by simply providing modified data in the user input _POST, which is read by the $PageLoad method at line 10 of /insecure.php. This input then flows through the code straight to the output web page, without sanitization. \r\n\r\nThis can enable a Reflected Cross-Site Scripting (XSS) attack.\n\n", descriptionHTML: - "\u003cp\u003eThe method $PageLoad embeds untrusted data in generated output with echo, at line 11 of /insecure.php. This untrusted data is embedded into the output without proper sanitization or encoding, enabling an attacker to inject malicious code into the generated web-page.\u003c/p\u003e\n\n\u003cp\u003eThe attacker would be able to alter the returned web page by simply providing modified data in the user input _POST, which is read by the $PageLoad method at line 10 of /insecure.php. This input then flows through the code straight to the output web page, without sanitization. \r\n\r\nThis can enable a Reflected Cross-Site Scripting (XSS) attack.\u003c/p\u003e\n", + "\u003cp\u003eThe method $PageLoad embeds untrusted data in generated output with echo, at line 11 of /insecure.php. This untrusted data is embedded into the output without proper sanitization or encoding, enabling an attacker to inject malicious code into the generated web-page.\u003c/p\u003e\n\n\u003cp\u003eThe attacker would be able to alter the returned web page by simply providing modified data in the user input _POST, which is read by the $PageLoad method at line 10 of /insecure.php. This input then flows through the code straight to the output web page, without sanitization. \r\n\r\nThis can enable a Reflected Cross-Site Scripting (XSS) attack.\u003c/p\u003e\n", data: { // eslint-disable-next-line @typescript-eslint/no-loss-of-precision queryId: 5157925289005576664, @@ -338,7 +338,7 @@ export class CxMock implements CxPlatform { domType: "UnknownReference", fileName: "/insecure.php", fullName: - "$NS_insecure_a3ce4a23.$Cls_insecure_a3ce4a23._POST", + "$NS_insecure_a3ce4a23.$Cls_insecure_a3ce4a23._POST", typeName: "CxDefaultObject", methodLine: 1, definitions: "1", @@ -434,9 +434,9 @@ export class CxMock implements CxPlatform { foundAt: "2023-04-21T10:34:40Z", firstScanId: "eaa0f3ea-ce32-4059-9836-db13d16fb2c8", description: - "It was found that apt-key in apt, all versions, do not correctly validate gpg keys with the master keyring, leading to a potential man-in-the-middle attack.", + "It was found that apt-key in apt, all versions, do not correctly validate gpg keys with the master keyring, leading to a potential man-in-the-middle attack.", descriptionHTML: - "\u003cp\u003eIt was found that apt-key in apt, all versions, do not correctly validate gpg keys with the master keyring, leading to a potential man-in-the-middle attack.\u003c/p\u003e\n", + "\u003cp\u003eIt was found that apt-key in apt, all versions, do not correctly validate gpg keys with the master keyring, leading to a potential man-in-the-middle attack.\u003c/p\u003e\n", data: {}, comments: {}, vulnerabilityDetails: { @@ -455,7 +455,7 @@ export class CxMock implements CxPlatform { type: "sscs-secret-detection", id: "49zX0DLkU5pXNroqUZ8IM57sO5U=", similarityId: - "7dd5481569c41f10fa3eff060673446480940fbc853b960b5b240029088ce639", + "7dd5481569c41f10fa3eff060673446480940fbc853b960b5b240029088ce639", status: "NEW", state: "TO_VERIFY", severity: "HIGH", @@ -473,11 +473,11 @@ export class CxMock implements CxPlatform { snippet: "eyJh***", slsaStep: "Source", ruleDescription: - "Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.", + "Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.", remediation: "Remove secret", remediationLink: null, remediationAdditional: - "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", + "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", validity: "Unknown", }, comments: {}, @@ -487,7 +487,7 @@ export class CxMock implements CxPlatform { type: "sscs-scorecard", id: "/5o5sN0v5K2I8J934plrSE6A8RQ=", similarityId: - "c7b19748c8985c3717885512828eb8b43962af069a71543fd7354c12f291dadc", + "c7b19748c8985c3717885512828eb8b43962af069a71543fd7354c12f291dadc", status: "NEW", state: "TO_VERIFY", severity: "HIGH", @@ -497,7 +497,7 @@ export class CxMock implements CxPlatform { foundAt: "2024-09-06T12:15:06Z", firstScanId: "b57170dc-fb77-442b-a9b5-0f788654b8ee", description: - "score is 0: no update tool detected:\nWarn: tool 'RenovateBot' is not used\nWarn: tool 'Dependabot' is not used\nWarn: tool 'PyUp' is not used", + "score is 0: no update tool detected:\nWarn: tool 'RenovateBot' is not used\nWarn: tool 'Dependabot' is not used\nWarn: tool 'PyUp' is not used", data: { ruleId: "DependencyUpdateToolID", ruleName: "Dependency-Update-Tool", @@ -506,11 +506,11 @@ export class CxMock implements CxPlatform { snippet: null, slsaStep: "Dependencies", ruleDescription: - "Determines if the project uses a dependency update tool.", + "Determines if the project uses a dependency update tool.", remediation: - "Implement the remediation recommendations provided in the URL", + "Implement the remediation recommendations provided in the URL", remediationLink: - "https://github.com/ossf/scorecard/blob/main/docs/checks.md#dependency-update-tool", + "https://github.com/ossf/scorecard/blob/main/docs/checks.md#dependency-update-tool", remediationAdditional: null, validity: null, }, @@ -521,7 +521,7 @@ export class CxMock implements CxPlatform { type: "sscs-scorecard", id: "A1hsrnELDUxAcOtjBGsP6GtlEBA=", similarityId: - "023c7542148f63613c00f016af227e6aa5e8c617d7ca53e7522feaf91539249f", + "023c7542148f63613c00f016af227e6aa5e8c617d7ca53e7522feaf91539249f", status: "NEW", state: "TO_VERIFY", severity: "HIGH", @@ -531,7 +531,7 @@ export class CxMock implements CxPlatform { foundAt: "2024-09-06T12:15:06Z", firstScanId: "b57170dc-fb77-442b-a9b5-0f788654b8ee", description: - "score is 0: 1 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0", + "score is 0: 1 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0", data: { ruleId: "MaintainedID", ruleName: "Maintained", @@ -540,11 +540,11 @@ export class CxMock implements CxPlatform { snippet: null, slsaStep: "Source", ruleDescription: - 'Determines if the project is "actively maintained".', + 'Determines if the project is "actively maintained".', remediation: - "Implement the remediation recommendations provided in the URL", + "Implement the remediation recommendations provided in the URL", remediationLink: - "https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained", + "https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained", remediationAdditional: null, validity: null, }, @@ -555,7 +555,7 @@ export class CxMock implements CxPlatform { type: "sscs-secret-detection", id: "AtXoUC0MXjbfw9pmPtR8LbsdvXk=", similarityId: - "4a6ddb4a8d5a9cbcbc8b4c45d35e710bd753b1cb824e594fc0c44831273fa53e", + "4a6ddb4a8d5a9cbcbc8b4c45d35e710bd753b1cb824e594fc0c44831273fa53e", status: "NEW", state: "TO_VERIFY", severity: "HIGH", @@ -565,7 +565,7 @@ export class CxMock implements CxPlatform { foundAt: "2024-09-06T12:15:06Z", firstScanId: "b57170dc-fb77-442b-a9b5-0f788654b8ee", description: - "stripe-access-token has detected secret for file /secrets.go.", + "stripe-access-token has detected secret for file /secrets.go.", data: { ruleId: "stripe-access-token", ruleName: "Stripe-Access-Token", @@ -574,11 +574,11 @@ export class CxMock implements CxPlatform { snippet: "sk_t***", slsaStep: "Source", ruleDescription: - "Found a Stripe Access Token, posing a risk to payment processing services and sensitive financial data.", + "Found a Stripe Access Token, posing a risk to payment processing services and sensitive financial data.", remediation: "Remove secret", remediationLink: null, remediationAdditional: - "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", + "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", validity: "Unknown", }, comments: {}, @@ -588,7 +588,7 @@ export class CxMock implements CxPlatform { type: "sscs-scorecard", id: "hJTinqcw2jva0HvKrXSM1hU3cgk=", similarityId: - "23fa634d6ae404d07586c36f4bb7eee8682805ed003e514354f89408eb0d840d", + "23fa634d6ae404d07586c36f4bb7eee8682805ed003e514354f89408eb0d840d", status: "NEW", state: "TO_VERIFY", severity: "LOW", @@ -598,7 +598,7 @@ export class CxMock implements CxPlatform { foundAt: "2024-09-06T12:15:06Z", firstScanId: "b57170dc-fb77-442b-a9b5-0f788654b8ee", description: - "score is 0: no effort to earn an OpenSSF best practices badge detected", + "score is 0: no effort to earn an OpenSSF best practices badge detected", data: { ruleId: "CIIBestPracticesID", ruleName: "CII-Best-Practices", @@ -607,11 +607,11 @@ export class CxMock implements CxPlatform { snippet: null, slsaStep: "Source", ruleDescription: - "Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.", + "Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.", remediation: - "Implement the remediation recommendations provided in the URL", + "Implement the remediation recommendations provided in the URL", remediationLink: - "https://github.com/ossf/scorecard/blob/main/docs/checks.md#cii-best-practices", + "https://github.com/ossf/scorecard/blob/main/docs/checks.md#cii-best-practices", remediationAdditional: null, validity: null, }, @@ -622,7 +622,7 @@ export class CxMock implements CxPlatform { type: "sscs-scorecard", id: "hzIL93W8mGkglJVJvtU5LWPFWCs=", similarityId: - "ff67df300cd3e181ce0d5c11534f17b05d1634427c0974737645fe6140546b7b", + "ff67df300cd3e181ce0d5c11534f17b05d1634427c0974737645fe6140546b7b", status: "NEW", state: "TO_VERIFY", severity: "HIGH", @@ -632,7 +632,7 @@ export class CxMock implements CxPlatform { foundAt: "2024-09-06T12:15:06Z", firstScanId: "b57170dc-fb77-442b-a9b5-0f788654b8ee", description: - "score is 0: branch protection not enabled on development/release branches:\nWarn: branch protection not enabled for branch 'main'", + "score is 0: branch protection not enabled on development/release branches:\nWarn: branch protection not enabled for branch 'main'", data: { ruleId: "BranchProtectionID", ruleName: "Branch-Protection", @@ -641,11 +641,11 @@ export class CxMock implements CxPlatform { snippet: null, slsaStep: "Source", ruleDescription: - "Determines if the default and release branches are protected with GitHub's branch protection settings.", + "Determines if the default and release branches are protected with GitHub's branch protection settings.", remediation: - "Implement the remediation recommendations provided in the URL", + "Implement the remediation recommendations provided in the URL", remediationLink: - "https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection", + "https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection", remediationAdditional: null, validity: null, }, @@ -656,7 +656,7 @@ export class CxMock implements CxPlatform { type: "sscs-secret-detection", id: "JiP6l2/7A1eZy/IJw5xb0K773VY=", similarityId: - "7dd5481569c41f10fa3eff060673446480940fbc853b960b5b240029088ce639", + "7dd5481569c41f10fa3eff060673446480940fbc853b960b5b240029088ce639", status: "NEW", state: "TO_VERIFY", severity: "HIGH", @@ -674,11 +674,11 @@ export class CxMock implements CxPlatform { snippet: "eyJh***", slsaStep: "Source", ruleDescription: - "Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.", + "Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.", remediation: "Remove secret", remediationLink: null, remediationAdditional: - "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", + "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", validity: "Unknown", }, comments: {}, @@ -688,7 +688,7 @@ export class CxMock implements CxPlatform { type: "sscs-secret-detection", id: "jIQToNyflbBa1oNDxw9KL/tRsJA=", similarityId: - "a28bc5175d1eea7caca193f7e419d331b063de90005eb0dee86caf5beaf5efef", + "a28bc5175d1eea7caca193f7e419d331b063de90005eb0dee86caf5beaf5efef", status: "NEW", state: "TO_VERIFY", severity: "HIGH", @@ -698,7 +698,7 @@ export class CxMock implements CxPlatform { foundAt: "2024-09-06T12:15:06Z", firstScanId: "b57170dc-fb77-442b-a9b5-0f788654b8ee", description: - "aws-access-token has detected secret for file /secrets.go.", + "aws-access-token has detected secret for file /secrets.go.", data: { ruleId: "aws-access-token", ruleName: "Aws-Access-Token", @@ -707,11 +707,11 @@ export class CxMock implements CxPlatform { snippet: "AKIA***", slsaStep: "Source", ruleDescription: - "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.", + "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.", remediation: "Remove secret", remediationLink: null, remediationAdditional: - "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", + "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", validity: "Unknown", }, comments: {}, @@ -721,7 +721,7 @@ export class CxMock implements CxPlatform { type: "sscs-scorecard", id: "lPQHovRXLnCOS084YFJztmmOgUs=", similarityId: - "6012016a4cf3c90eb32ccc1f002be3df48fd50c564a258b66b68d41102deabcf", + "6012016a4cf3c90eb32ccc1f002be3df48fd50c564a258b66b68d41102deabcf", status: "NEW", state: "TO_VERIFY", severity: "MEDIUM", @@ -731,7 +731,7 @@ export class CxMock implements CxPlatform { foundAt: "2024-09-06T12:15:06Z", firstScanId: "b57170dc-fb77-442b-a9b5-0f788654b8ee", description: - "score is 0: security policy file not detected:\nWarn: no security policy file detected\nWarn: no security file to analyze\nWarn: no security file to analyze\nWarn: no security file to analyze", + "score is 0: security policy file not detected:\nWarn: no security policy file detected\nWarn: no security file to analyze\nWarn: no security file to analyze\nWarn: no security file to analyze", data: { ruleId: "SecurityPolicyID", ruleName: "Security-Policy", @@ -740,11 +740,11 @@ export class CxMock implements CxPlatform { snippet: null, slsaStep: "Source", ruleDescription: - "Determines if the project has published a security policy.", + "Determines if the project has published a security policy.", remediation: - "Implement the remediation recommendations provided in the URL", + "Implement the remediation recommendations provided in the URL", remediationLink: - "https://github.com/ossf/scorecard/blob/main/docs/checks.md#security-policy", + "https://github.com/ossf/scorecard/blob/main/docs/checks.md#security-policy", remediationAdditional: null, validity: null, }, @@ -755,7 +755,7 @@ export class CxMock implements CxPlatform { type: "sscs-scorecard", id: "NEz6oBIVFxHpCSKo5eXrva9Kk9A=", similarityId: - "0475e70493b2eb38b92c8379ab51b0e7ca99732f0de79d09973931be8c439228", + "0475e70493b2eb38b92c8379ab51b0e7ca99732f0de79d09973931be8c439228", status: "NEW", state: "TO_VERIFY", severity: "HIGH", @@ -765,7 +765,7 @@ export class CxMock implements CxPlatform { foundAt: "2024-09-06T12:15:06Z", firstScanId: "b57170dc-fb77-442b-a9b5-0f788654b8ee", description: - "score is 0: found 2 unreviewed changesets out of 2 -- score normalized to 0", + "score is 0: found 2 unreviewed changesets out of 2 -- score normalized to 0", data: { ruleId: "CodeReviewID", ruleName: "Code-Review", @@ -774,11 +774,11 @@ export class CxMock implements CxPlatform { snippet: null, slsaStep: "Source", ruleDescription: - "Determines if the project requires human code review before pull requests (aka merge requests) are merged.", + "Determines if the project requires human code review before pull requests (aka merge requests) are merged.", remediation: - "Implement the remediation recommendations provided in the URL", + "Implement the remediation recommendations provided in the URL", remediationLink: - "https://github.com/ossf/scorecard/blob/main/docs/checks.md#code-review", + "https://github.com/ossf/scorecard/blob/main/docs/checks.md#code-review", remediationAdditional: null, validity: null, }, @@ -789,7 +789,7 @@ export class CxMock implements CxPlatform { type: "sscs-secret-detection", id: "qKGhcZ6Jd5mm4KaMTvg7c5d/+ro=", similarityId: - "a28bc5175d1eea7caca193f7e419d331b063de90005eb0dee86caf5beaf5efef", + "a28bc5175d1eea7caca193f7e419d331b063de90005eb0dee86caf5beaf5efef", status: "NEW", state: "TO_VERIFY", severity: "HIGH", @@ -799,7 +799,7 @@ export class CxMock implements CxPlatform { foundAt: "2024-09-06T12:15:06Z", firstScanId: "b57170dc-fb77-442b-a9b5-0f788654b8ee", description: - "aws-access-token has detected secret for file /secrets.go.", + "aws-access-token has detected secret for file /secrets.go.", data: { ruleId: "aws-access-token", ruleName: "Aws-Access-Token", @@ -808,11 +808,11 @@ export class CxMock implements CxPlatform { snippet: "AKIA***", slsaStep: "Source", ruleDescription: - "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.", + "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.", remediation: "Remove secret", remediationLink: null, remediationAdditional: - "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", + "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", validity: "Unknown", }, comments: {}, @@ -822,7 +822,7 @@ export class CxMock implements CxPlatform { type: "sscs-secret-detection", id: "TJWZcBouTD0JnYWO52XdEkQ2lpA=", similarityId: - "4a6ddb4a8d5a9cbcbc8b4c45d35e710bd753b1cb824e594fc0c44831273fa53e", + "4a6ddb4a8d5a9cbcbc8b4c45d35e710bd753b1cb824e594fc0c44831273fa53e", status: "NEW", state: "TO_VERIFY", severity: "HIGH", @@ -832,7 +832,7 @@ export class CxMock implements CxPlatform { foundAt: "2024-09-06T12:15:06Z", firstScanId: "b57170dc-fb77-442b-a9b5-0f788654b8ee", description: - "stripe-access-token has detected secret for file /secrets.go.", + "stripe-access-token has detected secret for file /secrets.go.", data: { ruleId: "stripe-access-token", ruleName: "Stripe-Access-Token", @@ -841,11 +841,11 @@ export class CxMock implements CxPlatform { snippet: "sk_t***", slsaStep: "Source", ruleDescription: - "Found a Stripe Access Token, posing a risk to payment processing services and sensitive financial data.", + "Found a Stripe Access Token, posing a risk to payment processing services and sensitive financial data.", remediation: "Remove secret", remediationLink: null, remediationAdditional: - "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", + "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", validity: "Unknown", }, comments: {}, @@ -855,7 +855,7 @@ export class CxMock implements CxPlatform { type: "sscs-scorecard", id: "tPes1zHT9L1Q5AP8T9iC5vZBG9U=", similarityId: - "da8f116022d814a1d338444a27719a71e19cb3083bd6fe618649a00878ae8c25", + "da8f116022d814a1d338444a27719a71e19cb3083bd6fe618649a00878ae8c25", status: "NEW", state: "TO_VERIFY", severity: "MEDIUM", @@ -865,7 +865,7 @@ export class CxMock implements CxPlatform { foundAt: "2024-09-06T12:15:06Z", firstScanId: "b57170dc-fb77-442b-a9b5-0f788654b8ee", description: - "score is 0: project is not fuzzed:\nWarn: no OSSFuzz integration found\nWarn: no GoBuiltInFuzzer integration found\nWarn: no PythonAtherisFuzzer integration found\nWarn: no CLibFuzzer integration found\nWarn: no CppLibFuzzer integration found\nWarn: no SwiftLibFuzzer integration found\nWarn: no RustCargoFuzzer integration found\nWarn: no JavaJazzerFuzzer integration found\nWarn: no ClusterFuzzLite integration found\nWarn: no HaskellPropertyBasedTesting integration found\nWarn: no TypeScriptPropertyBasedTesting integration found\nWarn: no JavaScriptPropertyBasedTesting integration found", + "score is 0: project is not fuzzed:\nWarn: no OSSFuzz integration found\nWarn: no GoBuiltInFuzzer integration found\nWarn: no PythonAtherisFuzzer integration found\nWarn: no CLibFuzzer integration found\nWarn: no CppLibFuzzer integration found\nWarn: no SwiftLibFuzzer integration found\nWarn: no RustCargoFuzzer integration found\nWarn: no JavaJazzerFuzzer integration found\nWarn: no ClusterFuzzLite integration found\nWarn: no HaskellPropertyBasedTesting integration found\nWarn: no TypeScriptPropertyBasedTesting integration found\nWarn: no JavaScriptPropertyBasedTesting integration found", data: { ruleId: "FuzzingID", ruleName: "Fuzzing", @@ -875,9 +875,9 @@ export class CxMock implements CxPlatform { slsaStep: "Package", ruleDescription: "Determines if the project uses fuzzing.", remediation: - "Implement the remediation recommendations provided in the URL", + "Implement the remediation recommendations provided in the URL", remediationLink: - "https://github.com/ossf/scorecard/blob/main/docs/checks.md#fuzzing", + "https://github.com/ossf/scorecard/blob/main/docs/checks.md#fuzzing", remediationAdditional: null, validity: null, }, @@ -888,7 +888,7 @@ export class CxMock implements CxPlatform { type: "sscs-secret-detection", id: "yfZPTrZsBH5X3BpleyRKUDS10BM=", similarityId: - "0870b429f0107daadc62c70e36cbb47683a7605b38a76b32922e3e0ac6f85f85", + "0870b429f0107daadc62c70e36cbb47683a7605b38a76b32922e3e0ac6f85f85", status: "NEW", state: "TO_VERIFY", severity: "HIGH", @@ -898,7 +898,7 @@ export class CxMock implements CxPlatform { foundAt: "2024-09-06T12:15:06Z", firstScanId: "b57170dc-fb77-442b-a9b5-0f788654b8ee", description: - "generic-api-key has detected secret for file /secrets.go.", + "generic-api-key has detected secret for file /secrets.go.", data: { ruleId: "generic-api-key", ruleName: "Generic-Api-Key", @@ -907,11 +907,11 @@ export class CxMock implements CxPlatform { snippet: "abc1***", slsaStep: "Source", ruleDescription: - "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", + "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", remediation: "Remove secret", remediationLink: null, remediationAdditional: - "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", + "Remove or mask the secret value shown in your file/artifact. If your secret is still valid, you should revoke it, in order to prevent malicious use of compromised secrets.", validity: "Unknown", }, comments: {}, @@ -921,11 +921,11 @@ export class CxMock implements CxPlatform { }; } writeFileSync( - getFilePath() + "/ast-results.json", - JSON.stringify(results), - { - flag: "w+", - } + getFilePath() + "/ast-results.json", + JSON.stringify(results), + { + flag: "w+", + } ); } @@ -980,7 +980,7 @@ export class CxMock implements CxPlatform { } async getProjectListWithParams( - params: string + params: string ): Promise { if (params) { if (this.getOffsetValue(params) === "0") { @@ -1217,8 +1217,8 @@ export class CxMock implements CxPlatform { } async getBranchesWithParams( - projectId: string | undefined, - params?: string | undefined + projectId: string | undefined, + params?: string | undefined ): Promise { if (params) { if (this.getBranchName(params) === "main") { @@ -1262,8 +1262,8 @@ export class CxMock implements CxPlatform { } async getScans( - projectId: string | undefined, - branch: string | undefined + projectId: string | undefined, + branch: string | undefined ): Promise { if (branch === constants.localBranch) { return []; @@ -1299,14 +1299,14 @@ export class CxMock implements CxPlatform { getBaseAstConfiguration() { const config = new CxConfig(); config.additionalParameters = vscode.workspace - .getConfiguration("checkmarxOne") - .get("additionalParams") as string; + .getConfiguration("checkmarxOne") + .get("additionalParams") as string; return config; } async getAstConfiguration() { - const token = await this.context.secrets.get("authCredential"); + const token = await this.context.secrets.get(constants.authCredentialSecretKey); if (!token) { return undefined; @@ -1329,6 +1329,18 @@ export class CxMock implements CxPlatform { return true; } + async isStandaloneEnabled(): Promise { + return false; + } + + async isCxOneAssistEnabled(): Promise { + return false; + } + + async isAuthenticated(): Promise { + return true; + } + async isAiMcpServerEnabled(): Promise { return true; } @@ -1386,12 +1398,12 @@ export class CxMock implements CxPlatform { queryName: "Reflected_XSS_All_Clients", queryDescriptionId: "Reflected_XSS_All_Clients", resultDescription: - "The method @DestinationMethod embeds untrusted data in generated output with @DestinationElement, at line @DestinationLine of @DestinationFile. This untrusted data is embedded into the output without proper sanitization or encoding, enabling an attacker to inject malicious code into the generated web-page.\n\nThe attacker would be able to alter the returned web page by simply providing modified data in the user input @SourceElement, which is read by the @SourceMethod method at line @SourceLine of @SourceFile. This input then flows through the code straight to the output web page, without sanitization. \r\n\r\nThis can enable a Reflected Cross-Site Scripting (XSS) attack.\n\n", + "The method @DestinationMethod embeds untrusted data in generated output with @DestinationElement, at line @DestinationLine of @DestinationFile. This untrusted data is embedded into the output without proper sanitization or encoding, enabling an attacker to inject malicious code into the generated web-page.\n\nThe attacker would be able to alter the returned web page by simply providing modified data in the user input @SourceElement, which is read by the @SourceMethod method at line @SourceLine of @SourceFile. This input then flows through the code straight to the output web page, without sanitization. \r\n\r\nThis can enable a Reflected Cross-Site Scripting (XSS) attack.\n\n", risk: "A successful XSS exploit would allow an attacker to rewrite web pages and insert malicious scripts which would alter the intended output. This could include HTML fragments, CSS styling rules, arbitrary JavaScript, or references to third party code. An attacker could use this to steal users' passwords, collect personal data such as credit card details, provide false information, or run malware. From the victim's point of view, this is performed by the genuine website, and the victim would blame the site for incurred damage.\n\nThe attacker could use social engineering to cause the user to send the website modified input, which will be returned in the requested web page.\n\n", cause: - "The application creates web pages that include untrusted data, whether from user input, the application's database, or from other external sources. The untrusted data is embedded directly in the page's HTML, causing the browser to display it as part of the web page. If the input includes HTML fragments or JavaScript, these are displayed too, and the user cannot tell that this is not the intended page. The vulnerability is the result of directly embedding arbitrary data without first encoding it in a format that would prevent the browser from treating it like HTML or code instead of plain text.\n\nNote that an attacker can exploit this vulnerability either by modifying the URL, or by submitting malicious data in the user input or other request fields.\n\n", + "The application creates web pages that include untrusted data, whether from user input, the application's database, or from other external sources. The untrusted data is embedded directly in the page's HTML, causing the browser to display it as part of the web page. If the input includes HTML fragments or JavaScript, these are displayed too, and the user cannot tell that this is not the intended page. The vulnerability is the result of directly embedding arbitrary data without first encoding it in a format that would prevent the browser from treating it like HTML or code instead of plain text.\n\nNote that an attacker can exploit this vulnerability either by modifying the URL, or by submitting malicious data in the user input or other request fields.\n\n", generalRecommendations: - '* Fully encode all dynamic data, regardless of source, before embedding it in output.\r\n* Encoding should be context-sensitive. For example:\r\n * HTML encoding for HTML content\r\n * HTML Attribute encoding for data output to attribute values\r\n * JavaScript encoding for server-generated JavaScript\r\n* It is recommended to use the platform-provided encoding functionality, or known security libraries for encoding output.\r\n* Implement a Content Security Policy (CSP) with explicit whitelists for the application\'s resources only. \r\n* As an extra layer of protection, validate all untrusted data, regardless of source (note this is not a replacement for encoding). Validation should be based on a whitelist: accept only data fitting a specified structure, rather than reject bad patterns. Check for:\r\n * Data type\r\n * Size\r\n * Range\r\n * Format\r\n * Expected values\r\n* In the `Content-Type` HTTP response header, explicitly define character encoding (charset) for the entire page. \r\n* Set the `HTTPOnly` flag on the session cookie for "Defense in Depth", to prevent any successful XSS exploits from stealing the cookie.\n* Consider that many native PHP methods for sanitizing values, such as htmlspecialchars and htmlentities, do not inherently encode values for Javascript contexts and ignore certain enclosure characters such as apostrophe (\'), quotes (") and backticks (\\`). Always consider the output context of inputs before choosing either of these functions as sanitizers.', + '* Fully encode all dynamic data, regardless of source, before embedding it in output.\r\n* Encoding should be context-sensitive. For example:\r\n * HTML encoding for HTML content\r\n * HTML Attribute encoding for data output to attribute values\r\n * JavaScript encoding for server-generated JavaScript\r\n* It is recommended to use the platform-provided encoding functionality, or known security libraries for encoding output.\r\n* Implement a Content Security Policy (CSP) with explicit whitelists for the application\'s resources only. \r\n* As an extra layer of protection, validate all untrusted data, regardless of source (note this is not a replacement for encoding). Validation should be based on a whitelist: accept only data fitting a specified structure, rather than reject bad patterns. Check for:\r\n * Data type\r\n * Size\r\n * Range\r\n * Format\r\n * Expected values\r\n* In the `Content-Type` HTTP response header, explicitly define character encoding (charset) for the entire page. \r\n* Set the `HTTPOnly` flag on the session cookie for "Defense in Depth", to prevent any successful XSS exploits from stealing the cookie.\n* Consider that many native PHP methods for sanitizing values, such as htmlspecialchars and htmlentities, do not inherently encode values for Javascript contexts and ignore certain enclosure characters such as apostrophe (\'), quotes (") and backticks (\\`). Always consider the output context of inputs before choosing either of these functions as sanitizers.', samples: [ { progLanguage: "PHP", @@ -1431,21 +1443,21 @@ export class CxMock implements CxPlatform { async runSastGpt() { await this.sleep(1000); return [ - {conversationId: "0", response: ["Mock message response from gpt"]}, + { conversationId: "0", response: ["Mock message response from gpt"] }, ]; } async runGpt() { await this.sleep(1000); return [ - {conversationId: "0", response: ["Mock message response from gpt"]}, + { conversationId: "0", response: ["Mock message response from gpt"] }, ]; } async mask() { await this.sleep(1000); return [ - {conversationId: "0", response: ["Mock message response from gpt"]}, + { conversationId: "0", response: ["Mock message response from gpt"] }, ]; } @@ -1454,9 +1466,9 @@ export class CxMock implements CxPlatform { } updateStatusBarItem( - text: string, - show: boolean, - statusBarItem: vscode.StatusBarItem + text: string, + show: boolean, + statusBarItem: vscode.StatusBarItem ) { statusBarItem.text = text; show ? statusBarItem.show() : statusBarItem.hide(); @@ -1514,8 +1526,8 @@ export class CxMock implements CxPlatform { } public getRiskManagementResults( - projectId: string, - scanId: string + projectId: string, + scanId: string ): Promise<{ projectID: string; scanID: string; diff --git a/src/extension.ts b/src/extension.ts index 8212e028..f1c1e1a6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode"; import { AstResultsProvider } from "./views/resultsView/astResultsProvider"; +import { AstResultsPromoProvider } from "./views/resultsView/astResultsPromoProvider"; import { constants } from "./utils/common/constants"; import { Logs } from "./models/logs"; import { @@ -21,6 +22,7 @@ import { FilterCommand } from "./commands/filterCommand"; import { WebViewCommand } from "./commands/webViewCommand"; import { WorkspaceListener } from "./utils/listener/workspaceListener"; import { DocAndFeedbackView } from "./views/docsAndFeedbackView/docAndFeedbackView"; +import { CxOneAssistProvider } from "./views/cxOneAssistView/cxOneAssistProvider"; import { messages } from "./utils/common/messages"; import { commands } from "./utils/common/commands"; import { IgnoredView } from "./views/ignoredView/ignoredView"; @@ -39,8 +41,228 @@ import { AscaScannerCommand } from "./realtimeScanners/scanners/asca/ascaScanner import { ContainersScannerCommand } from "./realtimeScanners/scanners/containers/containersScannerCommand"; import { registerMcpSettingsInjector } from "./services/mcpSettingsInjector"; +import { DOC_LINKS } from "./constants/documentation"; +import { cx } from "./cx"; let globalContext: vscode.ExtensionContext; +async function setupStatusBars(context: vscode.ExtensionContext, logs: Logs) { + const runScanStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + const runSCAScanStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + runSCAScanStatusBar.text = messages.scaStatusBarConnect; + + async function updateScaStatusBar() { + const isStandalone = await cx.isStandaloneEnabled(logs); + if (!isStandalone) { + runSCAScanStatusBar.show(); + } else { + runSCAScanStatusBar.hide(); + } + } + await updateScaStatusBar(); + context.subscriptions.push( + vscode.commands.registerCommand(commands.refreshScaStatusBar, async () => { + await updateScaStatusBar(); + }) + ); + + const kicsStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + const ignoredStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 20); + + // Central refresh command for KICS status bar + context.subscriptions.push( + vscode.commands.registerCommand(commands.refreshKicsStatusBar, async () => { + const standalone = await cx.isStandaloneEnabled(logs); + if (!standalone) { + kicsStatusBarItem.show(); + } else { + kicsStatusBarItem.hide(); + } + }) + ); + // Initial KICS status bar visibility + await vscode.commands.executeCommand(commands.refreshKicsStatusBar); + + return { + runScanStatusBar, + runSCAScanStatusBar, + kicsStatusBarItem, + statusBarItem, + ignoredStatusBarItem, + updateScaStatusBar + }; +} + +async function setupRealtimeScanners(context: vscode.ExtensionContext, logs: Logs) { + const configManager = new ConfigurationManager(); + const scannerRegistry = new ScannerRegistry(context, logs, configManager); + await scannerRegistry.activateAllScanners(); + const configListener = configManager.registerConfigChangeListener((section) => { + const ossEffected = section(`${constants.ossRealtimeScanner}.${constants.activateOssRealtimeScanner}`); + if (ossEffected) { + scannerRegistry.getScanner(constants.ossRealtimeScannerEngineName)?.register(); + return; + } + const secretsEffected = section(`${constants.secretsScanner}.${constants.activateSecretsScanner}`); + if (secretsEffected) { + scannerRegistry.getScanner(constants.secretsScannerEngineName)?.register(); + return; + } + const ascaEffected = section(`${constants.ascaRealtimeScanner}.${constants.activateAscaRealtimeScanner}`); + if (ascaEffected) { + scannerRegistry.getScanner(constants.ascaRealtimeScannerEngineName)?.register(); + return; + } + const containersEffected = section(`${constants.containersRealtimeScanner}.${constants.activateContainersRealtimeScanner}`); + if (containersEffected) { + scannerRegistry.getScanner(constants.containersRealtimeScannerEngineName)?.register(); + return; + } + const iacEffected = section(`${constants.iacRealtimeScanner}.${constants.activateIacRealtimeScanner}`); + if (iacEffected) { + scannerRegistry.getScanner(constants.iacRealtimeScannerEngineName)?.register(); + return; + } + }); + context.subscriptions.push(configListener); + + const ignoreFileManager = IgnoreFileManager.getInstance(); + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (workspaceFolder) { + ignoreFileManager.initialize(workspaceFolder); + } + + const ossCommand = scannerRegistry.getScanner(constants.ossRealtimeScannerEngineName) as OssScannerCommand; + const ossScanner = ossCommand.getScannerService(); + const secretCommand = scannerRegistry.getScanner(constants.secretsScannerEngineName) as SecretsScannerCommand; + const secretScanner = secretCommand.getScannerService(); + const iacCommand = scannerRegistry.getScanner(constants.iacRealtimeScannerEngineName) as IacScannerCommand; + const iacScanner = iacCommand.getScannerService(); + const ascaCommand = scannerRegistry.getScanner(constants.ascaRealtimeScannerEngineName) as AscaScannerCommand; + const ascaScanner = ascaCommand.getScannerService(); + const containersCommand = scannerRegistry.getScanner(constants.containersRealtimeScannerEngineName) as ContainersScannerCommand; + const containersScanner = containersCommand.getScannerService(); + + ignoreFileManager.setOssScannerService(ossScanner); + ignoreFileManager.setSecretsScannerService(secretScanner); + ignoreFileManager.setIacScannerService(iacScanner); + ignoreFileManager.setAscaScannerService(ascaScanner); + ignoreFileManager.setContainersScannerService(containersScanner); + context.subscriptions.push({ dispose: () => ignoreFileManager.dispose() }); + + return { scannerRegistry, ignoreFileManager, ossScanner, secretScanner, iacScanner, ascaScanner, containersScanner }; +} + +function setupKicsRealtime( + context: vscode.ExtensionContext, + logs: Logs, + kicsStatusBarItem: vscode.StatusBarItem, + kicsDiagnosticCollection: vscode.DiagnosticCollection +) { + const kicsProvider = new KicsProvider( + context, + logs, + kicsStatusBarItem, + kicsDiagnosticCollection, + [], + [] + ); + const kicsScanCommand = new KICSRealtimeCommand(context, kicsProvider, logs); + kicsScanCommand.registerKicsScans(); + return { kicsProvider, kicsScanCommand }; +} + +function setupIgnoredStatusBar( + context: vscode.ExtensionContext, + logs: Logs, + ignoreFileManager: IgnoreFileManager, + ignoredStatusBarItem: vscode.StatusBarItem, + cxOneAssistProvider: CxOneAssistProvider +) { + async function updateIgnoredStatusBar() { + if (await cx.isValidConfiguration() && await cx.isCxOneAssistEnabled(logs)) { + const count = ignoreFileManager.getIgnoredPackagesCount(); + const hasIgnoreFile = ignoreFileManager.hasIgnoreFile(); + if (hasIgnoreFile) { + ignoredStatusBarItem.text = `$(circle-slash) ${count}`; + ignoredStatusBarItem.tooltip = count > 0 + ? `${count} ignored vulnerabilities - Click to view` + : `No ignored vulnerabilities - Click to view`; + ignoredStatusBarItem.command = commands.openIgnoredView; + ignoredStatusBarItem.show(); + } else { + ignoredStatusBarItem.hide(); + } + cxOneAssistProvider.updateWebviewContent(); + } else { + ignoredStatusBarItem.hide(); + } + } + context.subscriptions.push( + vscode.commands.registerCommand(commands.refreshIgnoredStatusBar, async () => { + await updateIgnoredStatusBar(); + }) + ); + ignoreFileManager.setStatusBarUpdateCallback(updateIgnoredStatusBar); + updateIgnoredStatusBar(); + return { updateIgnoredStatusBar }; +} + +// --- Helper wrappers for refactored snippet --- +function registerAssistDocumentation(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand(commands.assistDocumentation, () => { + vscode.env.openExternal(vscode.Uri.parse(DOC_LINKS.devAssist)); + }) + ); +} + +function registerPromoResultsWebview(context: vscode.ExtensionContext, logs: Logs) { + const promoProvider = new AstResultsPromoProvider(context, logs); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(commands.astResultsPromo, promoProvider) + ); + return promoProvider; +} + +function registerScaPromoWebview(context: vscode.ExtensionContext, logs: Logs) { + // Reuse AstResultsPromoProvider for SCA promo; adjust later if SCA-specific content needed + const promoProvider = new AstResultsPromoProvider(context, logs); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(commands.scaAutoScanPromo, promoProvider) + ); + return promoProvider; +} + +function registerAssistView(context: vscode.ExtensionContext, ignoreFileManager: IgnoreFileManager, logs: Logs) { + const cxOneAssistProvider = new CxOneAssistProvider(context, ignoreFileManager, logs); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(commands.astCxOneAssist, cxOneAssistProvider) + ); + return cxOneAssistProvider; +} + +function registerAssistRelatedCommands(context: vscode.ExtensionContext, cxOneAssistProvider: CxOneAssistProvider) { + context.subscriptions.push( + vscode.commands.registerCommand(commands.updateCxOneAssist, async () => { + await cxOneAssistProvider.onAuthenticationChanged(); + }) + ); + context.subscriptions.push( + vscode.commands.registerCommand(commands.authentication, () => { + vscode.commands.executeCommand(commands.showAuth); + }) + ); +} + +function registerAuthenticationLauncher(context: vscode.ExtensionContext, webViewCommand: WebViewCommand, logs: Logs) { + context.subscriptions.push( + vscode.commands.registerCommand(commands.showAuth, () => { + AuthenticationWebview.show(context, webViewCommand, logs); + }) + ); +} + export async function activate(context: vscode.ExtensionContext) { // Initialize cx first initialize(context); @@ -54,69 +276,21 @@ export async function activate(context: vscode.ExtensionContext) { // Integrity check on startup const authService = AuthService.getInstance(context, logs); await authService.validateAndUpdateState(); + // Register docs & promo webview now that logs exist + registerAssistDocumentation(context); + registerPromoResultsWebview(context, logs); + registerScaPromoWebview(context, logs); + + // --- Setup grouped UI elements --- + const { + runScanStatusBar, + runSCAScanStatusBar, + kicsStatusBarItem, + statusBarItem, + ignoredStatusBarItem + } = await setupStatusBars(context, logs); - // Status bars creation - const runScanStatusBar = vscode.window.createStatusBarItem( - vscode.StatusBarAlignment.Left - ); - const runSCAScanStatusBar = vscode.window.createStatusBarItem( - vscode.StatusBarAlignment.Left - ); - runSCAScanStatusBar.text = messages.scaStatusBarConnect; - runSCAScanStatusBar.show(); - const kicsStatusBarItem = vscode.window.createStatusBarItem( - vscode.StatusBarAlignment.Left - ); - const statusBarItem = vscode.window.createStatusBarItem( - vscode.StatusBarAlignment.Left - ); - const ignoredStatusBarItem = vscode.window.createStatusBarItem( - vscode.StatusBarAlignment.Left, 20 - - ); - - const configManager = new ConfigurationManager(); - const scannerRegistry = new ScannerRegistry(context, logs, configManager); - await scannerRegistry.activateAllScanners(); - const configListener = configManager.registerConfigChangeListener( - (section) => { - const ossEffected = section( - `${constants.ossRealtimeScanner}.${constants.activateOssRealtimeScanner}` - ); - if (ossEffected) { - scannerRegistry.getScanner(constants.ossRealtimeScannerEngineName)?.register(); - return; - } - const secretsEffected = section( - `${constants.secretsScanner}.${constants.activateSecretsScanner}` - ); - if (secretsEffected) { - scannerRegistry.getScanner(constants.secretsScannerEngineName)?.register(); - return; - } - const ascaEffected = section( - `${constants.ascaRealtimeScanner}.${constants.activateAscaRealtimeScanner}` - ); - if (ascaEffected) { - scannerRegistry.getScanner(constants.ascaRealtimeScannerEngineName)?.register(); - return; - } - const containersEffected = section( - `${constants.containersRealtimeScanner}.${constants.activateContainersRealtimeScanner}` - ); - if (containersEffected) { - scannerRegistry.getScanner(constants.containersRealtimeScannerEngineName)?.register(); - return; - } - const iacEffected = section( - `${constants.iacRealtimeScanner}.${constants.activateIacRealtimeScanner}` - ); - if (iacEffected) { - scannerRegistry.getScanner(constants.iacRealtimeScannerEngineName)?.register(); - return; - } - }); - context.subscriptions.push(configListener); + const { ignoreFileManager, ossScanner, secretScanner, iacScanner, ascaScanner, containersScanner } = await setupRealtimeScanners(context, logs); await setScanButtonDefaultIfScanIsNotRunning(context); @@ -129,16 +303,16 @@ export async function activate(context: vscode.ExtensionContext) { constants.extensionName ); - const kicsProvider = new KicsProvider( - context, - logs, - kicsStatusBarItem, - kicsDiagnosticCollection, - [], - [] + // Command to allow other components (e.g., auth webview) to clear KICS remediation diagnostics + context.subscriptions.push( + vscode.commands.registerCommand(commands.clearKicsDiagnostics, async () => { + if (await cx.isStandaloneEnabled(logs)) { + kicsDiagnosticCollection.clear(); + } + }) ); - const kicsScanCommand = new KICSRealtimeCommand(context, kicsProvider, logs); - kicsScanCommand.registerKicsScans(); + + const { kicsScanCommand } = setupKicsRealtime(context, logs, kicsStatusBarItem, kicsDiagnosticCollection); const diagnosticCollection = vscode.languages.createDiagnosticCollection( constants.extensionName @@ -206,7 +380,7 @@ export async function activate(context: vscode.ExtensionContext) { // Documentation & Feedback view const docAndFeedback = new DocAndFeedbackView(); - const docAndFeedbackTree = vscode.window.createTreeView("docAndFeedback", { + const docAndFeedbackTree = vscode.window.createTreeView(commands.docAndFeedback, { treeDataProvider: docAndFeedback, }); @@ -218,9 +392,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.env.openExternal(vscode.Uri.parse(url)); } } - }); - - // SCA auto scanning commands register + }); // SCA auto scanning commands register const scaScanCommand = new ScanSCACommand( context, runSCAScanStatusBar, @@ -235,7 +407,10 @@ export async function activate(context: vscode.ExtensionContext) { const scaTree = vscode.window.createTreeView(constants.scaTreeName, { treeDataProvider: scaResultsProvider, }); - scaTree.onDidChangeSelection((item) => { + scaTree.onDidChangeSelection(async (item) => { + if (await cx.isStandaloneEnabled(this.logs)) { + return; + } if (item.selection.length > 0) { if (!item.selection[0].contextValue && !item.selection[0].children) { // Open new details @@ -256,6 +431,10 @@ export async function activate(context: vscode.ExtensionContext) { commonCommand.executeCheckSettings(); // Scan from IDE enablement await commonCommand.executeCheckScanEnabled(); + + await commonCommand.executeCheckStandaloneEnabled(); + + await commonCommand.executeCheckCxOneAssistEnabled(); // SCA auto scanning enablement await commonCommand.executeCheckScaScanEnabled(); // execute command to listen to settings change @@ -271,6 +450,8 @@ export async function activate(context: vscode.ExtensionContext) { scaResultsProvider, logs ); + // Promo webview already registered above + // Register refresh sca and results Tree Command treeCommand.registerRefreshCommands(); // Register clear sca and results tree Command @@ -289,46 +470,19 @@ export async function activate(context: vscode.ExtensionContext) { // Refresh sca tree with start scan message scaResultsProvider.refreshData(constants.scaStartScan); - // Register authentication command - - context.subscriptions.push( - vscode.commands.registerCommand("ast-results.showAuth", () => { - AuthenticationWebview.show(context, webViewCommand, logs); - }) - ); - const ignoreFileManager = IgnoreFileManager.getInstance(); - const ossCommand = scannerRegistry.getScanner(constants.ossRealtimeScannerEngineName) as OssScannerCommand; - const ossScanner = ossCommand.getScannerService(); - - const secretCommand = scannerRegistry.getScanner(constants.secretsScannerEngineName) as SecretsScannerCommand; - const secretScanner = secretCommand.getScannerService(); - - const iacCommand = scannerRegistry.getScanner(constants.iacRealtimeScannerEngineName) as IacScannerCommand; - const iacScanner = iacCommand.getScannerService(); + // Authentication launcher + registerAuthenticationLauncher(context, webViewCommand, logs); + // ignoreFileManager already initialized & wired in setupRealtimeScanners - const ascaCommand = scannerRegistry.getScanner(constants.ascaRealtimeScannerEngineName) as AscaScannerCommand; - const ascaScanner = ascaCommand.getScannerService(); - - const containersCommand = scannerRegistry.getScanner(constants.containersRealtimeScannerEngineName) as ContainersScannerCommand; - const containersScanner = containersCommand.getScannerService(); - - ignoreFileManager.setOssScannerService(ossScanner); - ignoreFileManager.setSecretsScannerService(secretScanner); - ignoreFileManager.setIacScannerService(iacScanner); - ignoreFileManager.setAscaScannerService(ascaScanner); - ignoreFileManager.setContainersScannerService(containersScanner); - context.subscriptions.push({ - dispose: () => ignoreFileManager.dispose() - }); + // CxOne Assist view & its commands + const cxOneAssistProvider = registerAssistView(context, ignoreFileManager, logs); + registerAssistRelatedCommands(context, cxOneAssistProvider); const copilotChatCommand = new CopilotChatCommand(context, logs, ossScanner, secretScanner, iacScanner, ascaScanner, containersScanner); registerMcpSettingsInjector(context); - copilotChatCommand.registerCopilotChatCommand(); - - const ignoredView = new IgnoredView(context); context.subscriptions.push( @@ -337,29 +491,7 @@ export async function activate(context: vscode.ExtensionContext) { }) ); - function updateIgnoredStatusBar() { - const count = ignoreFileManager.getIgnoredPackagesCount(); - const hasIgnoreFile = ignoreFileManager.hasIgnoreFile(); - - if (hasIgnoreFile) { - ignoredStatusBarItem.text = `$(circle-slash) ${count}`; - ignoredStatusBarItem.tooltip = count > 0 - ? `${count} ignored vulnerabilities - Click to view` - : `No ignored vulnerabilities - Click to view`; - ignoredStatusBarItem.command = commands.openIgnoredView; - ignoredStatusBarItem.show(); - } else { - ignoredStatusBarItem.hide(); - } - } - - updateIgnoredStatusBar(); - - ignoreFileManager.setStatusBarUpdateCallback(updateIgnoredStatusBar); - - - - + setupIgnoredStatusBar(context, logs, ignoreFileManager, ignoredStatusBarItem, cxOneAssistProvider); vscode.commands.registerCommand("ast-results.mockTokenTest", async () => { const authService = AuthService.getInstance(context); diff --git a/src/kics/kicsRealtimeProvider.ts b/src/kics/kicsRealtimeProvider.ts index ef15a98d..30c1e567 100644 --- a/src/kics/kicsRealtimeProvider.ts +++ b/src/kics/kicsRealtimeProvider.ts @@ -38,7 +38,7 @@ export class KicsProvider { : messages.kicsStatusBarDisconnect; this.kicsStatusBarItem.tooltip = messages.kicsAutoScan; this.kicsStatusBarItem.command = commands.kicsSetings; - this.kicsStatusBarItem.show(); + vscode.commands.executeCommand(commands.refreshKicsStatusBar); this.fixableResults = []; this.fixableResultsByLine = []; } @@ -62,7 +62,7 @@ export class KicsProvider { this.kicsStatusBarItem.text = messages.kicsAutoScanRunning; this.kicsStatusBarItem.tooltip = messages.kicsRunning; - this.kicsStatusBarItem.show(); + await vscode.commands.executeCommand(commands.refreshKicsStatusBar); // Get current file, either from global state or from the current open file const file = await this.getCurrentFile(this.context, this.logs); if (!file) { diff --git a/src/services/authService.ts b/src/services/authService.ts index f1f58a9f..d49923b7 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -5,6 +5,7 @@ import { URL, URLSearchParams } from 'url'; import { Logs } from '../models/logs'; import { getCx, initialize } from '../cx'; import { commands } from "../utils/common/commands"; +import { constants } from "../utils/common/constants"; import { ProxyHelper } from '../utils/proxy/proxy'; import axios from "axios"; @@ -295,8 +296,7 @@ export class AuthService { public async validateApiKey(apiKey: string): Promise { try { - - await this.context.secrets.store("authCredential", apiKey); + await this.context.secrets.store(constants.authCredentialSecretKey, apiKey); const cx = getCx(); return await cx.authValidate(this.logs); @@ -360,8 +360,7 @@ export class AuthService { } public async saveToken(context: vscode.ExtensionContext, token: string) { - - await this.context.secrets.store("authCredential", token); + await this.context.secrets.store(constants.authCredentialSecretKey, token); console.log("Token stored in secrets"); const isValid = await this.validateAndUpdateState(); console.log("Token validation result:", isValid); @@ -369,14 +368,14 @@ export class AuthService { if (isValid) { vscode.window.showInformationMessage("Successfully authenticated to Checkmarx One server"); await vscode.commands.executeCommand(commands.refreshTree); + await vscode.commands.executeCommand(commands.updateCxOneAssist); } } public async validateAndUpdateState(): Promise { try { - const token = await this.context.secrets.get("authCredential"); - + const token = await this.context.secrets.get(constants.authCredentialSecretKey); if (!token) { vscode.commands.executeCommand( @@ -417,16 +416,23 @@ export class AuthService { } public async getToken(): Promise { - return await this.context.secrets.get("authCredential"); + return await this.context.secrets.get(constants.authCredentialSecretKey); } public async logout(): Promise { // Delete only the token - await this.context.secrets.delete("authCredential"); + await this.context.secrets.delete(constants.authCredentialSecretKey); + await this.context.secrets.delete(constants.standaloneEnabledGlobalState); await this.validateAndUpdateState(); await vscode.commands.executeCommand(commands.refreshTree); await vscode.commands.executeCommand(commands.clear); + + await vscode.commands.executeCommand(commands.updateCxOneAssist); + await vscode.commands.executeCommand( + commands.setContext, + commands.isStandaloneEnabled, + false); } private getSuccessPageHtml(): string { diff --git a/src/services/mcpSettingsInjector.ts b/src/services/mcpSettingsInjector.ts index b05893c0..49f6a8f8 100644 --- a/src/services/mcpSettingsInjector.ts +++ b/src/services/mcpSettingsInjector.ts @@ -35,7 +35,7 @@ const checkmarxMcpServerName = "Checkmarx"; export function registerMcpSettingsInjector(context: vscode.ExtensionContext) { vscode.commands.registerCommand("ast-results.installMCP", async () => { - const apikey = await context.secrets.get("authCredential"); + const apikey = await context.secrets.get(constants.authCredentialSecretKey); if (!apikey) { vscode.window.showErrorMessage("Failed in install Checkmarx MCP: Authentication required"); return; @@ -182,7 +182,7 @@ export async function initializeMcpConfiguration(apiKey: string) { ], disabled: false, autoApprove: ["codeRemediation", "imageRemediation", "packageRemediation"] - } + }; await updateMcpJsonFile(kiroMcpServer); return; } diff --git a/src/test/5.customActions.test.ts b/src/test/5.customActions.test.ts index 03050bab..aaba54cc 100644 --- a/src/test/5.customActions.test.ts +++ b/src/test/5.customActions.test.ts @@ -49,7 +49,7 @@ describe("filter and groups actions tests", () => { } }); - it("should click on all group by", async function () { + it.skip("should click on all group by", async function () { const commands = [ CX_GROUP_LANGUAGE, CX_GROUP_STATUS, @@ -80,7 +80,7 @@ describe("filter and groups actions tests", () => { expect(tuple[0]).to.be.at.most(4); }); - it("should click on all group by", async function () { + it.skip("should click on all group by", async function () { const commands = [CX_GROUP_LANGUAGE, CX_GROUP_STATUS, CX_GROUP_STATE, CX_GROUP_QUERY_NAME, CX_GROUP_FILE]; // Get scan node const treeScans = await initialize(); diff --git a/src/test/8.NoResult.test.ts b/src/test/8.NoResult.test.ts index 5fa734b2..cd205e14 100644 --- a/src/test/8.NoResult.test.ts +++ b/src/test/8.NoResult.test.ts @@ -8,7 +8,7 @@ import { Workbench, } from "vscode-extension-tester"; import { expect } from "chai"; -import { getDetailsView, getResults, initialize, waitForNotificationWithTimeout } from "./utils/utils"; +import { getDetailsView, getResults, initialize, waitForNotificationWithTimeout, sleep } from "./utils/utils"; import { CHANGES_CONTAINER, CHANGES_LABEL, CODEBASHING_HEADER, COMMENT_BOX, CX_LOOK_SCAN, GENERAL_LABEL, LEARN_MORE_LABEL, SAST_TYPE, SCAN_KEY_TREE_LABEL, UPDATE_BUTTON, WEBVIEW_TITLE } from "./utils/constants"; import { waitByClassName } from "./utils/waiters"; import { EMPTY_RESULTS_SCAN_ID, SCAN_ID } from "./utils/envs"; @@ -35,6 +35,7 @@ describe("Scan ID load results test", () => { let input = await new InputBox(); await input.setText("e3b2505a-0634-4b41-8fa1-dfeb2edc26f7"); await input.confirm(); + sleep(5000) }); it("should check scan result is not undefined", async function () { @@ -46,22 +47,22 @@ describe("Scan ID load results test", () => { let scan = await treeScans?.findItem( SCAN_KEY_TREE_LABEL ); - await scan?.expand(); + await scan?.expand(); let scanChildren = await scan?.getChildren(); let scanResults = await scanChildren[0].getChildren(); expect(scanResults).not.to.be.undefined; expect(scanResults.length).to.be.equal(0); - }); + }); it("should allow creating a new scan even if the current scan has zero results", async function () { - + await bench.executeCommand(CX_LOOK_SCAN); const input = await InputBox.create(); await input.setText(EMPTY_RESULTS_SCAN_ID); await input.confirm(); - + await bench.executeCommand("ast-results.createScan"); - let firstNotification = await waitForNotificationWithTimeout(5000) + let firstNotification = await waitForNotificationWithTimeout(5000) let message = await firstNotification?.getMessage(); if (message === messages.scanProjectNotMatch) { let actions = await firstNotification?.getActions() @@ -70,5 +71,5 @@ describe("Scan ID load results test", () => { firstNotification = await waitForNotificationWithTimeout(5000); } expect(firstNotification).to.not.be.undefined; - }); -}); + }); +}); \ No newline at end of file diff --git a/src/unit/kicsRealtimeCommand.test.ts b/src/unit/kicsRealtimeCommand.test.ts index 6d8a23f3..08ec1b61 100644 --- a/src/unit/kicsRealtimeCommand.test.ts +++ b/src/unit/kicsRealtimeCommand.test.ts @@ -1,61 +1,91 @@ import { expect } from "chai"; import "./mocks/vscode-mock"; import { KICSRealtimeCommand } from "../commands/kicsRealtimeCommand"; -import { Logs } from "../models/logs"; +import { commands } from "../utils/common/commands"; +import { cx } from "../cx"; +import { getRegisteredCommandCallback, clearCommandsExecuted } from "./mocks/vscode-mock"; import * as vscode from "vscode"; -import { clearCommandsExecuted } from "./mocks/vscode-mock"; +import { Logs } from "../models/logs"; import { KicsProvider } from "../kics/kicsRealtimeProvider"; -describe("KICSRealtimeCommand", () => { - let kicsCommand: KICSRealtimeCommand; - let logs: Logs; - let mockContext: vscode.ExtensionContext; - let mockKicsProvider: KicsProvider; - +class StubProvider extends KicsProvider { + runKicsIfEnabledCalled = false; + kicsRemediationCalled = false; + constructor(context: vscode.ExtensionContext, logs: Logs) { + super( + context, + logs, + { text: "", tooltip: "", command: undefined, show: () => { }, hide: () => { }, dispose: () => { } } as vscode.StatusBarItem, + { set: () => { }, delete: () => { }, clear: () => { } } as unknown as vscode.DiagnosticCollection, + [], + [] + ); + } + async runKicsIfEnabled() { this.runKicsIfEnabledCalled = true; } + async kicsRemediation(): Promise { this.kicsRemediationCalled = true; } +} - beforeEach(() => { - clearCommandsExecuted(); - - const mockOutputChannel = { - append: () => {}, - appendLine: () => {}, - clear: () => {}, - show: () => {}, - hide: () => {}, - dispose: () => {}, - replace: () => {}, - name: "Test" - }; +const logs: Logs = { + info: () => { }, + error: () => { }, + warn: () => { }, + show: () => { }, + output: { append: () => { }, appendLine: () => { }, clear: () => { }, show: () => { }, hide: () => { }, dispose: () => { }, replace: () => { }, name: "Test" }, + log: () => { } +} as Logs; - logs = { - info: () => {}, - error: () => {}, - output: mockOutputChannel, - log: () => {}, - warn: () => {}, - show: () => {} - } as Logs; +describe("KICSRealtimeCommand standalone gating", () => { + let prevStandalone: typeof cx.isStandaloneEnabled; + before(() => { prevStandalone = cx.isStandaloneEnabled; }); + after(() => { cx.isStandaloneEnabled = prevStandalone; }); + beforeEach(() => { clearCommandsExecuted(); }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockContext = { subscriptions: [] } as any; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockKicsProvider = { runKicsIfEnabled: async () => {} } as any; + it("skip kics realtime scan when standalone enabled", async () => { + cx.isStandaloneEnabled = async () => true; + type MockContext = { subscriptions: unknown[]; secrets: { get: (key: string) => Promise }; globalState: { get: (key: string) => unknown; update: (key: string, value: unknown) => Promise } }; + const context: MockContext = { subscriptions: [], secrets: { get: async () => "token" }, globalState: { get: () => undefined, update: async () => { } } }; + const provider = new StubProvider(context as unknown as vscode.ExtensionContext, logs); + const cmd = new KICSRealtimeCommand(context as unknown as vscode.ExtensionContext, provider, logs); + cmd.registerKicsScans(); + const cb = getRegisteredCommandCallback(commands.kicsRealtime); + expect(cb).to.be.a("function"); + await cb(); + expect(provider.runKicsIfEnabledCalled).to.equal(false); + }); - kicsCommand = new KICSRealtimeCommand(mockContext, mockKicsProvider, logs); + it("run kics realtime scan when standalone disabled", async () => { + cx.isStandaloneEnabled = async () => false; + type MockContext = { subscriptions: unknown[]; secrets: { get: (key: string) => Promise }; globalState: { get: (key: string) => unknown; update: (key: string, value: unknown) => Promise } }; + const context: MockContext = { subscriptions: [], secrets: { get: async () => "token" }, globalState: { get: () => undefined, update: async () => { } } }; + const provider = new StubProvider(context as unknown as vscode.ExtensionContext, logs); + const cmd = new KICSRealtimeCommand(context as unknown as vscode.ExtensionContext, provider, logs); + cmd.registerKicsScans(); + const cb = getRegisteredCommandCallback(commands.kicsRealtime); + await cb(); + expect(provider.runKicsIfEnabledCalled).to.equal(true); }); - describe("registerKicsScans", () => { - it("should register kics scan command", () => { - kicsCommand.registerKicsScans(); - expect(mockContext.subscriptions.length).to.be.greaterThan(0); - }); + it("skip kicsremediation when standalone enabled", async () => { + cx.isStandaloneEnabled = async () => true; + type MockContext = { subscriptions: unknown[]; secrets: { get: (key: string) => Promise }; globalState: { get: (key: string) => unknown; update: (key: string, value: unknown) => Promise } }; + const context: MockContext = { subscriptions: [], secrets: { get: async () => "token" }, globalState: { get: () => undefined, update: async () => { } } }; + const provider = new StubProvider(context as unknown as vscode.ExtensionContext, logs); + const cmd = new KICSRealtimeCommand(context as unknown as vscode.ExtensionContext, provider, logs); + cmd.registerKicsRemediation(); + const cb = getRegisteredCommandCallback(commands.kicsRemediation); + await cb([], [], { file: "dummy" }, {}, false, false); + expect(provider.kicsRemediationCalled).to.equal(false); }); - describe("registerSettings", () => { - it("should register kics settings command", () => { - kicsCommand.registerSettings(); - expect(mockContext.subscriptions.length).to.be.greaterThan(0); - }); + it("run kics remediation when standalone disabled", async () => { + cx.isStandaloneEnabled = async () => false; + type MockContext = { subscriptions: unknown[]; secrets: { get: (key: string) => Promise }; globalState: { get: (key: string) => unknown; update: (key: string, value: unknown) => Promise } }; + const context: MockContext = { subscriptions: [], secrets: { get: async () => "token" }, globalState: { get: () => undefined, update: async () => { } } }; + const provider = new StubProvider(context as unknown as vscode.ExtensionContext, logs); + const cmd = new KICSRealtimeCommand(context as unknown as vscode.ExtensionContext, provider, logs); + cmd.registerKicsRemediation(); + const cb = getRegisteredCommandCallback(commands.kicsRemediation); + await cb([], [], { file: "dummy" }, {}, false, false); + expect(provider.kicsRemediationCalled).to.equal(true); }); -}); \ No newline at end of file +}); diff --git a/src/unit/mocks/vscode-mock.ts b/src/unit/mocks/vscode-mock.ts index 5d7c9cf8..2c54ec36 100644 --- a/src/unit/mocks/vscode-mock.ts +++ b/src/unit/mocks/vscode-mock.ts @@ -6,6 +6,7 @@ import { constants } from "../../utils/common/constants"; import * as sinon from "sinon"; let commandsExecuted: string[] = []; +const registeredCallbacks: Record any> = {}; const mockDiagnosticCollection = { set: sinon.stub(), @@ -35,6 +36,16 @@ const mock = { }, }; } + if (section === constants.cxKics) { + return { + get: (key: string) => { + if (key === constants.cxKicsAutoScan) { + return false; // autoscan disabled by default in tests + } + return undefined; + } + }; + } return undefined; }, workspaceFolders: [{ uri: { fsPath: "/mock/path" } }], @@ -78,13 +89,14 @@ const mock = { }, commands: { - executeCommand: (command: string) => { + executeCommand: (command: string, ..._args: any[]) => { commandsExecuted.push(command); return Promise.resolve(); }, getCommands: () => Promise.resolve([]), registerCommand: (command: string, callback: (...args: any[]) => any) => { - return { dispose: () => { } }; + registeredCallbacks[command] = callback; + return { dispose: () => { delete registeredCallbacks[command]; } }; } }, @@ -182,4 +194,5 @@ mockRequire("vscode", mock); export { mock, mockDiagnosticCollection, resetMocks }; export const getCommandsExecuted = () => commandsExecuted; -export const clearCommandsExecuted = () => { commandsExecuted = []; }; \ No newline at end of file +export const clearCommandsExecuted = () => { commandsExecuted = []; }; +export const getRegisteredCommandCallback = (command: string) => registeredCallbacks[command]; \ No newline at end of file diff --git a/src/utils/common/commands.ts b/src/utils/common/commands.ts index 4a24e295..a3888e2f 100644 --- a/src/utils/common/commands.ts +++ b/src/utils/common/commands.ts @@ -9,11 +9,13 @@ export const commands = { setings: `${constants.extensionName}.viewSettings`, isValidCredentials: `${constants.extensionName}.isValidCredentials`, + authentication: `${constants.extensionName}.authentication`, showAuth: `${constants.extensionName}.showAuth`, isScanEnabled: `${constants.extensionName}.isScanEnabled`, isScaScanEnabled: `${constants.extensionName}.isSCAScanEnabled`, - + isStandaloneEnabled: `${constants.extensionName}.isStandaloneEnabled`, + isCxOneAssistEnabled: `${constants.extensionName}.isCxOneAssistEnabled`, filterCriticalToggle: `${constants.extensionName}.filterCritical_toggle`, filterCriticalUntoggle: `${constants.extensionName}.filterCritical_untoggle`, filterCritical: `${constants.extensionName}.filterCritical`, @@ -128,5 +130,15 @@ export const commands = { ignorePackage: `${constants.extensionName}.${constants.ignorePackage}`, ignoreAll: `${constants.extensionName}.${constants.ignoreAll}`, openIgnoredView: `${constants.extensionName}.openIgnoredView`, - + refreshIgnoredStatusBar: `${constants.extensionName}.refreshIgnoredStatusBar`, + refreshScaStatusBar: `${constants.extensionName}.refreshScaStatusBar`, + refreshKicsStatusBar: `${constants.extensionName}.refreshKicsStatusBar`, + assistDocumentation: `${constants.extensionName}.assistDocumentation`, + updateCxOneAssist: `${constants.extensionName}.updateCxOneAssist`, + astCxOneAssist: "astCxOneAssist", + astResultsPromo: "astResultsPromo", + scaAutoScanPromo: "scaAutoScanPromo", + docAndFeedback: "docAndFeedback", + refreshRiskManagementView: `${constants.extensionName}.refreshRiskManagementView`, + clearKicsDiagnostics: `${constants.extensionName}.clearKicsDiagnostics` }; diff --git a/src/utils/common/configValidators.ts b/src/utils/common/configValidators.ts new file mode 100644 index 00000000..a4c6eb19 --- /dev/null +++ b/src/utils/common/configValidators.ts @@ -0,0 +1,16 @@ +import { cx } from "../../cx"; +import { Logs } from "../../models/logs"; + +/** + * Determines whether operations dependent on a full configuration should proceed. + * Returns true only when Checkmarx configuration is valid AND standalone mode is disabled. + * Centralized so all commands use identical gating logic. + */ +export async function validateConfigurationAndLicense(logs: Logs): Promise { + const isValid = await cx.isValidConfiguration(); + if (!isValid) { + return false; + } + const isStandalone = await cx.isStandaloneEnabled(logs); + return !isStandalone; +} diff --git a/src/utils/common/constants.ts b/src/utils/common/constants.ts index 0a75fe82..9e8258af 100644 --- a/src/utils/common/constants.ts +++ b/src/utils/common/constants.ts @@ -1,6 +1,9 @@ export const constants = { extensionName: "ast-results", extensionFullName: "Checkmarx", + standaloneEnabledGlobalState: "standaloneEnabled", + cxOneAssistEnabledGlobalState: "cxOneAssistEnabled", + authCredentialSecretKey: "authCredential", scanIdKey: "ast-results-scan-id", scanCreateIdKey: "ast-results-scan-create-id", scanCreatePrepKey: "ast-results-scan-prep-id", diff --git a/src/utils/listener/listeners.ts b/src/utils/listener/listeners.ts index 92218dde..dcdf8cd2 100644 --- a/src/utils/listener/listeners.ts +++ b/src/utils/listener/listeners.ts @@ -87,6 +87,10 @@ export function addRealTimeSaveListener( ) { // Listen to save action in a KICS file vscode.workspace.onDidSaveTextDocument(async (e) => { + // Skip scan trigger in standalone mode + if (await cx.isStandaloneEnabled(logs)) { + return; + } // Check if on save setting is enabled const isValidKicsFile = isKicsFile(e); const isSystemFiles = isSystemFile(e); @@ -111,6 +115,10 @@ export function addRealTimeSaveListener( // Listen to open action in a KICS file vscode.workspace.onDidOpenTextDocument(async (e: vscode.TextDocument) => { + // Skip scan trigger in standalone mode + if (await cx.isStandaloneEnabled(logs)) { + return; + } // Check if on save setting is enabled const isValidKicsFile = isKicsFile(e); const isSystemFiles = isSystemFile(e); diff --git a/src/views/cxOneAssistView/CxOneAssistTypes.ts b/src/views/cxOneAssistView/CxOneAssistTypes.ts new file mode 100644 index 00000000..ecee14a9 --- /dev/null +++ b/src/views/cxOneAssistView/CxOneAssistTypes.ts @@ -0,0 +1,20 @@ +import * as vscode from "vscode"; +import { IgnoreFileManager } from "../../realtimeScanners/common/ignoreFileManager"; + +export interface CxOneAssistDependencies { + context: vscode.ExtensionContext; + ignoreFileManager: IgnoreFileManager; +} + +export interface CxOneAssistWebviewState { + ignoredCount: number; + hasIgnoreFile: boolean; + isStandaloneEnabled: boolean; + isAuthenticated: boolean; + isCxOneAssistEnabled: boolean; +} + +export interface CxOneAssistMessage { + command: string; + data?: unknown; +} \ No newline at end of file diff --git a/src/views/cxOneAssistView/CxOneAssistUtils.ts b/src/views/cxOneAssistView/CxOneAssistUtils.ts new file mode 100644 index 00000000..decad215 --- /dev/null +++ b/src/views/cxOneAssistView/CxOneAssistUtils.ts @@ -0,0 +1,33 @@ +import { IgnoreFileManager } from "../../realtimeScanners/common/ignoreFileManager"; +import { CxOneAssistWebviewState } from "./CxOneAssistTypes"; +import * as vscode from "vscode"; +import { Cx } from "../../cx/cx"; +import { Logs } from "../../models/logs"; + +export class CxOneAssistUtils { + public static async getWebviewState( + ignoreFileManager: IgnoreFileManager, + context: vscode.ExtensionContext, + logs: Logs + ): Promise { + const ignoredCount = ignoreFileManager.getIgnoredPackagesCount(); + const hasIgnoreFile = ignoreFileManager.hasIgnoreFile(); + const cxInstance = new Cx(context); + const isAuthenticated = await cxInstance.isValidConfiguration(); + const isStandaloneEnabled = await cxInstance.isStandaloneEnabled(logs); + const isCxOneAssistEnabled = await cxInstance.isCxOneAssistEnabled(logs); + return { ignoredCount, hasIgnoreFile, isStandaloneEnabled, isAuthenticated, isCxOneAssistEnabled }; + } + + public static shouldShowIgnoredButton(state: CxOneAssistWebviewState): boolean { + return state.hasIgnoreFile && state.ignoredCount > 0; + } + + public static formatIgnoredText(count: number): string { + return count === 0 ? "No ignored vulnerabilities" : `View ignored vulnerabilities (${count})`; + } + + public static getIgnoredTooltip(count: number): string { + return count === 0 ? "No ignored vulnerabilities found" : `${count} ignored vulnerabilities - Click to view`; + } +} \ No newline at end of file diff --git a/src/views/cxOneAssistView/CxOneAssistWebview.ts b/src/views/cxOneAssistView/CxOneAssistWebview.ts new file mode 100644 index 00000000..88556d43 --- /dev/null +++ b/src/views/cxOneAssistView/CxOneAssistWebview.ts @@ -0,0 +1,419 @@ +import * as vscode from "vscode"; +import * as path from "path"; +import { CxOneAssistWebviewState } from "./CxOneAssistTypes"; +import { CxOneAssistUtils } from "./CxOneAssistUtils"; + +export class CxOneAssistWebview { + public static generateHtml( + context: vscode.ExtensionContext, + webview: vscode.Webview, + state: CxOneAssistWebviewState + ): string { + const cubeImageUri = webview.asWebviewUri( + vscode.Uri.file(path.join(context.extensionPath, "media", "icons", "cxone-assist-cube.svg")) + ); + + const showIgnoredButton = CxOneAssistUtils.shouldShowIgnoredButton(state); + const ignoredText = CxOneAssistUtils.formatIgnoredText(state.ignoredCount); + const ignoredTooltip = CxOneAssistUtils.getIgnoredTooltip(state.ignoredCount); + + return ` + + + + + CxOne Assist + + + +
+
+
+ CxOne Assist 3D Cube +
+ +
+ CxOne Assist provides real-time threat detection and helps you avoid vulnerabilities before they happen. +
+ + ${showIgnoredButton ? ` +
+ ` : ''} +
+ + + + `; + } + + private static getStyles(): string { + return ` + body { + margin: 0; + padding: 16px; + font-family: var(--vscode-font-family); + background: var(--vscode-sideBar-background); + color: var(--vscode-sideBar-foreground); + overflow-x: hidden; + } + + .assist-container { + text-align: center; + position: relative; + } + + .cube-container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + } + + .cube-image { + width: 100%; + height: 100%; + } + + .description { + font-size: 13px; + line-height: 1.4; + color: var(--vscode-descriptionForeground); + margin: 5px 0; + text-align: left; + } + + #view-ignored-vuln-btn { + text-align: left; + } + + .action-link { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--vscode-textLink-foreground); + text-decoration: none; + font-size: 13px; + padding: 0px; + cursor: pointer; + border: none; + background: none; + border-radius: 4px; + transition: all 0.2s ease; + } + + .action-link:hover { + color: var(--vscode-textLink-activeForeground); + background: var(--vscode-list-hoverBackground); + } + + .action-icon { + font-size: 14px; + } + + .particles { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + overflow: hidden; + } + + .particle { + position: absolute; + width: 2px; + height: 2px; + background: var(--vscode-textLink-foreground); + border-radius: 50%; + opacity: 0.6; + animation: particleFloat 8s infinite linear; + } + + @keyframes float { + 0%, 100% { + transform: translateY(0px) rotateY(0deg); + } + 50% { + transform: translateY(-10px) rotateY(180deg); + } + } + + @keyframes particleFloat { + from { + transform: translateY(200px) translateX(0px); + opacity: 0; + } + 10% { + opacity: 0.6; + } + 90% { + opacity: 0.6; + } + to { + transform: translateY(-20px) translateX(20px); + opacity: 0; + } + } + + .hidden { + display: none !important; + } + `; + } + + private static getScript(): string { + return ` + const vscode = acquireVsCodeApi(); + + function createParticles() { + const container = document.getElementById('particles'); + if (!container) return; + + for (let i = 0; i < 8; i++) { + setTimeout(() => { + const particle = document.createElement('div'); + particle.className = 'particle'; + particle.style.left = Math.random() * 100 + '%'; + particle.style.animationDelay = Math.random() * 3 + 's'; + particle.style.animationDuration = (Math.random() * 4 + 6) + 's'; + container.appendChild(particle); + + setTimeout(() => { + if (particle.parentNode) { + particle.parentNode.removeChild(particle); + } + }, 10000); + }, i * 500); + } + } + + function openIgnoredView() { + vscode.postMessage({ + command: 'openIgnoredView' + }); + } + + createParticles(); + setInterval(createParticles, 8000); + + window.addEventListener('message', event => { + const message = event.data; + if (message.command === 'updateState') { + updateUI(message.state); + } + }); + + function updateUI(state) { + const button = document.getElementById('ignored-vuln-button'); + if (!button) return; + + const shouldShow = state.hasIgnoreFile && state.ignoredCount > 0; + + if (shouldShow) { + button.style.display = 'inline-flex'; + button.textContent = state.ignoredCount === 0 + ? 'No ignored vulnerabilities' + : \`View ignored vulnerabilities (\${state.ignoredCount})\`; + button.title = state.ignoredCount === 0 + ? 'No ignored vulnerabilities found' + : \`Click to view \${state.ignoredCount} ignored vulnerabilities\`; + } else { + button.style.display = 'none'; + } + } + `; + } + + public static renderDisabledStandaloneHtml( + context: vscode.ExtensionContext, + webview: vscode.Webview + ): string { + const cubeImageUri = webview.asWebviewUri( + vscode.Uri.file(path.join(context.extensionPath, "media", "icons", "cxone-assist-cube.svg")) + ); + + return ` + + + + + CxOne Assist + + + + + + + + `; + } + + private static getUnauthenticatedStyles(): string { + return ` + body { + margin: 0; + padding: 16px; + font-family: var(--vscode-font-family); + background: var(--vscode-sideBar-background); + color: var(--vscode-sideBar-foreground); + overflow-x: hidden; + } + + .login-container { + text-align: center; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 200px; + } + + .cube-container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + } + + .cube-image { + width: 100%; + height: 100%; + } + + .login-title { + font-size: 16px; + font-weight: bold; + color: var(--vscode-descriptionForeground); + text-align: center; + } + + .login-description { + font-size: 13px; + line-height: 1.4; + color: var(--vscode-descriptionForeground); + margin: 10px 0 10px 0; + text-align: center; + } + + .login-button { + color: var(--vscode-descriptionForeground); + border-radius: 4px; + font-size: 13px; + font-family: var(--vscode-font-family); + transition: all 0.2s ease; + outline: none; + } + + .particles { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + overflow: hidden; + } + + .particle { + position: absolute; + width: 2px; + height: 2px; + background: var(--vscode-textLink-foreground); + border-radius: 50%; + opacity: 0.6; + animation: particleFloat 8s infinite linear; + } + + @keyframes float { + 0%, 100% { + transform: translateY(0px) rotateY(0deg); + } + 50% { + transform: translateY(-10px) rotateY(180deg); + } + } + + @keyframes particleFloat { + from { + transform: translateY(200px) translateX(0px); + opacity: 0; + } + 10% { + opacity: 0.6; + } + 90% { + opacity: 0.6; + } + to { + transform: translateY(-20px) translateX(20px); + opacity: 0; + } + } + `; + } + + private static getUnauthenticatedScript(): string { + return ` + // VS Code API + const vscode = acquireVsCodeApi(); + + // Create floating particles + function createParticles() { + const container = document.getElementById('particles'); + if (!container) return; + + for (let i = 0; i < 6; i++) { + setTimeout(() => { + const particle = document.createElement('div'); + particle.className = 'particle'; + particle.style.left = Math.random() * 100 + '%'; + particle.style.animationDelay = Math.random() * 3 + 's'; + particle.style.animationDuration = (Math.random() * 4 + 6) + 's'; + container.appendChild(particle); + + setTimeout(() => { + if (particle.parentNode) { + particle.parentNode.removeChild(particle); + } + }, 10000); + }, i * 500); + } + } + + // Initialize + createParticles(); + setInterval(createParticles, 8000); + `; + } +} \ No newline at end of file diff --git a/src/views/cxOneAssistView/cxOneAssistProvider.ts b/src/views/cxOneAssistView/cxOneAssistProvider.ts new file mode 100644 index 00000000..250adbe7 --- /dev/null +++ b/src/views/cxOneAssistView/cxOneAssistProvider.ts @@ -0,0 +1,144 @@ +import * as vscode from "vscode"; +import { CxOneAssistDependencies, CxOneAssistMessage, CxOneAssistWebviewState } from "./CxOneAssistTypes"; +import { CxOneAssistWebview } from "./CxOneAssistWebview"; +import { CxOneAssistUtils } from "./CxOneAssistUtils"; +import { IgnoreFileManager } from "../../realtimeScanners/common/ignoreFileManager"; +import { Logs } from "../../models/logs"; + +export class CxOneAssistProvider implements vscode.WebviewViewProvider { + private webviewView?: vscode.WebviewView; + private currentState: CxOneAssistWebviewState; + private readonly dependencies: CxOneAssistDependencies; + private logs: Logs; + + constructor(context: vscode.ExtensionContext, ignoreFileManager: IgnoreFileManager, logs: Logs) { + this.dependencies = { context, ignoreFileManager }; + this.currentState = { ignoredCount: 0, hasIgnoreFile: false, isStandaloneEnabled: false, isAuthenticated: false, isCxOneAssistEnabled: false }; + this.logs = logs; + } + + public resolveWebviewView( + webviewView: vscode.WebviewView, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _context: vscode.WebviewViewResolveContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _token: vscode.CancellationToken, + ): void { + this.webviewView = webviewView; + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [vscode.Uri.file(this.dependencies.context.extensionPath + "/media")] + }; + + this.updateWebviewContent(); + this.setupMessageHandling(); + } + + public async updateWebviewContent(): Promise { + if (!this.webviewView) { + return; + } + + this.currentState = await CxOneAssistUtils.getWebviewState( + this.dependencies.ignoreFileManager, + this.dependencies.context, + this.logs + ); + + if (!this.currentState.isAuthenticated) { + this.webviewView.webview.html = this.renderUnauthenticatedHtml(); + } + else if (this.currentState.isCxOneAssistEnabled) { + this.webviewView.webview.html = CxOneAssistWebview.generateHtml( + this.dependencies.context, + this.webviewView.webview, + this.currentState + ); + } + else { + this.webviewView.webview.html = CxOneAssistWebview.renderDisabledStandaloneHtml( + this.dependencies.context, + this.webviewView.webview + ); + } + } + + private renderUnauthenticatedHtml(): string { + const nonce = this.getNonce(); + return ` + + + + + + + +
+
+

In order to use Checkmarx One Assist, you need to setup your credentials.

+
+ +
+

To learn more about how to use Checkmarx One Assist read our docs.

+
+
+ + + `; + } + + private getNonce(): string { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 16; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; + } + + public async onAuthenticationChanged(): Promise { + await this.updateWebviewContent(); + } + + private setupMessageHandling(): void { + if (!this.webviewView) { + return; + } + + this.webviewView.webview.onDidReceiveMessage( + (message: CxOneAssistMessage) => { + this.handleWebviewMessage(message); + } + ); + } + + private handleWebviewMessage(message: CxOneAssistMessage): void { + switch (message.command) { + case 'openIgnoredView': + vscode.commands.executeCommand('ast-results.openIgnoredView'); + break; + case 'openSettings': + vscode.commands.executeCommand('ast-results.viewSettings'); + break; + default: + console.warn(`Unknown command received from CxOne Assist webview: ${message.command}`); + } + } +} \ No newline at end of file diff --git a/src/views/resultsView/astResultsPromoProvider.ts b/src/views/resultsView/astResultsPromoProvider.ts new file mode 100644 index 00000000..149abcdf --- /dev/null +++ b/src/views/resultsView/astResultsPromoProvider.ts @@ -0,0 +1,42 @@ +import * as vscode from 'vscode'; +import { PromotionalCardView } from '../shared/PromotionalCardView'; +import { Logs } from '../../models/logs'; + +export class AstResultsPromoProvider implements vscode.WebviewViewProvider { + private _view?: vscode.WebviewView; + + constructor(private readonly context: vscode.ExtensionContext, private readonly logs: Logs) { } + + resolveWebviewView(webviewView: vscode.WebviewView): void | Thenable { + this._view = webviewView; + webviewView.webview.options = { enableScripts: true }; + this.render(); + webviewView.webview.onDidReceiveMessage(msg => { + PromotionalCardView.handleMessage(msg); + }); + } + + private render() { + const promoConfig = PromotionalCardView.getAspmConfig(); + const styles = PromotionalCardView.generateStyles(); + const html = PromotionalCardView.generateHtml(promoConfig); + const script = PromotionalCardView.generateScript(); + if (!this._view) { return; } + this._view.webview.html = ` + + + + + + + + + ${html} + + + `; + } +} diff --git a/src/views/resultsView/astResultsProvider.ts b/src/views/resultsView/astResultsProvider.ts index 28b2d684..467ef454 100644 --- a/src/views/resultsView/astResultsProvider.ts +++ b/src/views/resultsView/astResultsProvider.ts @@ -17,6 +17,7 @@ import CxResult from "@checkmarxdev/ast-cli-javascript-wrapper/dist/main/results import { getResultsWithProgress } from "../../utils/pickers/pickers"; import { ResultsProvider } from "../resultsProviders"; import { riskManagementView } from '../riskManagementView/riskManagementView'; +import { validateConfigurationAndLicense } from "../../utils/common/configValidators"; export class AstResultsProvider extends ResultsProvider { public process; @@ -35,7 +36,7 @@ export class AstResultsProvider extends ResultsProvider { super(context, statusBarItem); this.loadedResults = undefined; - this.riskManagementView = new riskManagementView(context.extensionUri, context); + this.riskManagementView = new riskManagementView(context.extensionUri, context, logs); context.subscriptions.push( vscode.window.registerWebviewViewProvider( @@ -43,6 +44,11 @@ export class AstResultsProvider extends ResultsProvider { this.riskManagementView ) ); + context.subscriptions.push( + vscode.commands.registerCommand(commands.refreshRiskManagementView, async () => { + this.riskManagementView.updateContent(); + }) + ); // Syncing with AST everytime the extension gets opened this.openRefreshData() @@ -62,25 +68,33 @@ export class AstResultsProvider extends ResultsProvider { } async refreshData(): Promise { - this.showStatusBarItem(messages.commandRunning); - const treeItem = await this.generateTree(); - this.data = await cx.isValidConfiguration() ? treeItem.children : []; - this._onDidChangeTreeData.fire(undefined); - this.hideStatusBarItem(); + if (await validateConfigurationAndLicense(this.logs)) { + this.showStatusBarItem(messages.commandRunning); + const treeItem = await this.generateTree(); + this.data = treeItem.children; + this._onDidChangeTreeData.fire(undefined); + this.hideStatusBarItem(); + } + else { + this.data = []; + this._onDidChangeTreeData.fire(undefined); + } } async openRefreshData(): Promise { - this.showStatusBarItem(messages.commandRunning); - this.loadedResults = undefined; - const scanIDItem = getFromState(this.context, constants.scanIdKey); - let scanId = undefined; - if (scanIDItem && scanIDItem.name) { - scanId = getFromState(this.context, constants.scanIdKey).name; - } - if (scanId) { - await getResultsWithProgress(this.logs, scanId); - await vscode.commands.executeCommand(commands.refreshTree); - this.hideStatusBarItem(); + if (await validateConfigurationAndLicense(this.logs)) { + this.showStatusBarItem(messages.commandRunning); + this.loadedResults = undefined; + const scanIDItem = getFromState(this.context, constants.scanIdKey); + let scanId = undefined; + if (scanIDItem && scanIDItem.name) { + scanId = getFromState(this.context, constants.scanIdKey).name; + } + if (scanId) { + await getResultsWithProgress(this.logs, scanId); + await vscode.commands.executeCommand(commands.refreshTree); + this.hideStatusBarItem(); + } } } diff --git a/src/views/riskManagementView/riskManagementView.ts b/src/views/riskManagementView/riskManagementView.ts index 8c5881a6..a686be0a 100644 --- a/src/views/riskManagementView/riskManagementView.ts +++ b/src/views/riskManagementView/riskManagementView.ts @@ -11,18 +11,24 @@ import { AstResult } from "../../models/results"; import { constants } from "../../utils/common/constants"; import CxResult from "@checkmarxdev/ast-cli-javascript-wrapper/dist/main/results/CxResult"; import { ICONS } from "./constants"; +import { PromotionalCardView } from "../shared/PromotionalCardView"; +import { cx } from "../../cx"; +import { Logs } from "../../models/logs"; export class riskManagementView implements vscode.WebviewViewProvider { private view?: vscode.WebviewView; private riskManagementService: riskManagementService; private cxResults: CxResult[] = []; + private logs: Logs; constructor( private readonly _extensionUri: vscode.Uri, - private readonly context: vscode.ExtensionContext + private readonly context: vscode.ExtensionContext, + logs: Logs ) { this.riskManagementService = riskManagementService.getInstance( this.context ); + this.logs = logs; } public async resolveWebviewView(webviewView: vscode.WebviewView) { @@ -47,6 +53,7 @@ export class riskManagementView implements vscode.WebviewViewProvider { private async handleMessage(message: { command: string; result?: { hash: string, riskScore: number, severity: string, traits: { [key: string]: string } }; + url?: string; }): Promise { switch (message.command) { case "openVulnerabilityDetails": { @@ -59,6 +66,16 @@ export class riskManagementView implements vscode.WebviewViewProvider { } break; } + case "openPromotionalLink": { + PromotionalCardView.handleMessage(message); + break; + } + case "openLearnMore": { + if (message.url) { + vscode.env.openExternal(vscode.Uri.parse(message.url)); + } + break; + } } } @@ -98,15 +115,26 @@ export class riskManagementView implements vscode.WebviewViewProvider { this.cxResults = cxResults; + const isStandalone = await cx.isStandaloneEnabled(this.logs); + if (isStandalone) { + const promoConfig = PromotionalCardView.getAspmConfig(); + const promoCardHtml = PromotionalCardView.generateHtml(promoConfig); + const promoCardStyles = PromotionalCardView.generateStyles(); + const promoCardScript = PromotionalCardView.generateScript(); + this.view.webview.html = await this.getPromoWebviewContent( + promoCardHtml, + promoCardStyles, + promoCardScript + ); + this.view.webview.postMessage({ command: "hideLoader" }); + return; + } if (!project && !scan) { - this.view.webview.html = this.getWebviewContent( + this.view.webview.html = await this.getAspmWebviewContent( undefined, undefined, false, - { - applicationNameIDMap: [], - results: undefined, - } + { results: undefined, applicationNameIDMap: [] } ); this.view.webview.postMessage({ command: "hideLoader" }); return; @@ -118,16 +146,16 @@ export class riskManagementView implements vscode.WebviewViewProvider { ); const projectToDisplay = this.extractData(project.name, "Project:"); const scanToDisplay = this.extractData(scan.displayScanId, "Scan:"); - const riskManagementResults = - await this.riskManagementService.getRiskManagementResults( - project.id, - scan.id - ); - this.view.webview.html = this.getWebviewContent( + interface RiskManagementResults { results: CxResult[]; applicationNameIDMap: { name: string; id: string }[] } + const riskManagementResults = await this.riskManagementService.getRiskManagementResults( + project.id, + scan.id + ) as RiskManagementResults; + this.view.webview.html = await this.getAspmWebviewContent( projectToDisplay, scanToDisplay, isLatestScan, - riskManagementResults as { results: any; applicationNameIDMap: any[] } + riskManagementResults ); this.view.webview.postMessage({ command: "getRiskManagementResults", @@ -168,7 +196,7 @@ export class riskManagementView implements vscode.WebviewViewProvider { } private async isAuthenticated(): Promise { - const token = await this.context.secrets.get("authCredential"); + const token = await this.context.secrets.get(constants.authCredentialSecretKey); return !!token; } @@ -192,12 +220,11 @@ export class riskManagementView implements vscode.WebviewViewProvider { ); } - private getWebviewContent( - projectName: string, - scan: string, - isLatestScan: boolean, - ASPMResults: { results: any; applicationNameIDMap: any[] } - ): string { + private async getPromoWebviewContent( + promoCardHtml: string, + promoCardStyles: string, + promoCardScript: string + ): Promise { const styleResetUri = this.setWebUri("media", "reset.css"); const styleVSCodeUri = this.setWebUri("media", "vscode.css"); const styleMainUri = this.setWebUri("media", "main.css"); @@ -230,110 +257,113 @@ export class riskManagementView implements vscode.WebviewViewProvider { return ` - - - - - - - - - - - - Risks Management - - - -
-
- ASPM Results Loading... -
-
-
- ${ - !projectName || !scan || !isLatestScan - ? `
- ASPM data is only shown when the most recent scan of a project is selected - in the Checkmarx One Results tab -
` - : ASPMResults.applicationNameIDMap.length === 0 - ? `
- This project is not associated with any application in the ASPM -
` - : ASPMResults.applicationNameIDMap.length > 0 && - ASPMResults.results.length === 0 - ? `
- ASPM does not hold result data for this project -
` - : `
- -
Project: - ${projectName ?? ""}
-
Scan ID: ${ - scan ?? "" - }
-
- -
-
- ${ICONS.union} ${ASPMResults.applicationNameIDMap.length} - Applications -
-
- -
-
SORT BY
-
Application Risk Score
-
Application Name A-Z
-
Application Name Z-A
-
-
-
- -
- -
-
Showing
- -
- - Vulnerability Type - -
- - - -
- - Additional Trait - -
- - -
+ + + + + + + + + + Risks Management + + + +
+
ASPM Results Loading...
-
-
+ ${promoCardHtml} + + + + + +`; + } -
- ` - }
- - - - - + private async getAspmWebviewContent( + projectName: string, + scan: string, + isLatestScan: boolean, + aspmResults: { results: CxResult[]; applicationNameIDMap: { name: string; id: string }[] } + ): Promise { + const styleResetUri = this.setWebUri("media", "reset.css"); + const styleVSCodeUri = this.setWebUri("media", "vscode.css"); + const styleMainUri = this.setWebUri("media", "main.css"); + const scriptUri = this.setWebUri("media", "riskManagement.js"); + const styleUri = this.setWebUri("media", "riskManagement.css"); + const codiconsUri = this.setWebUri("node_modules", "@vscode/codicons", "dist", "codicon.css"); + const popperUri = this.setWebUri("node_modules", "@popperjs/core", "dist", "umd", "popper.min.js"); + const styleBootStrap = this.setWebUri("media", "bootstrap", "bootstrap.min.css"); + const scriptBootStrap = this.setWebUri("media", "bootstrap", "bootstrap.min.js"); + const nonce = getNonce(); + return ` + + + + + + + + + + + Risks Management + + +
+
ASPM Results Loading...
+
+
+ ${!projectName || !scan || !isLatestScan + ? `
ASPM data is only shown when the most recent scan of a project is selected in the Checkmarx One Results tab
` + : aspmResults.applicationNameIDMap.length === 0 + ? `
This project is not associated with any application in the ASPM
` + : aspmResults.applicationNameIDMap.length > 0 && aspmResults.results.length === 0 + ? `
ASPM does not hold result data for this project
` + : `
+
Project: ${projectName ?? ""}
+
Scan ID: ${scan ?? ""}
+
+
+
+ ${ICONS.union} ${aspmResults.applicationNameIDMap.length} Applications +
+
+ +
+
SORT BY
+
Application Risk Score
+
Application Name A-Z
+
Application Name Z-A
+
+
+
+
+ +
+
Showing
+
Vulnerability Type
+ +
Additional Trait
+ +
+
+
+
+
`} +
+ + + + + `; } } diff --git a/src/views/scaView/scaResultsProvider.ts b/src/views/scaView/scaResultsProvider.ts index 00b4187d..8e07ee62 100644 --- a/src/views/scaView/scaResultsProvider.ts +++ b/src/views/scaView/scaResultsProvider.ts @@ -10,6 +10,7 @@ import { messages } from "../../utils/common/messages"; import { orderResults } from "../../utils/utils"; import { ResultsProvider } from "../resultsProviders"; import CxScaRealTimeErrors from "@checkmarxdev/ast-cli-javascript-wrapper/dist/main/scaRealtime/CxScaRealTimeErrors"; +import { cx } from "../../cx"; export class SCAResultsProvider extends ResultsProvider { public process; @@ -40,11 +41,17 @@ export class SCAResultsProvider extends ResultsProvider { } async refreshData(message?: string): Promise { - this.showStatusBarItem(constants.scaScanRunningLog); - this.message = message ? message : messages.scaStartScan; - this.data = this.generateTree().children; - this._onDidChangeTreeData.fire(undefined); - this.hideStatusBarItem(); + if (!await cx.isStandaloneEnabled(this.logs)) { + this.showStatusBarItem(constants.scaScanRunningLog); + this.message = message ? message : messages.scaStartScan; + this.data = this.generateTree().children; + this._onDidChangeTreeData.fire(undefined); + this.hideStatusBarItem(); + } + else{ + this.data = []; + this._onDidChangeTreeData.fire(undefined); + } } diff --git a/src/views/shared/PromotionalCardView.ts b/src/views/shared/PromotionalCardView.ts new file mode 100644 index 00000000..edacdfe7 --- /dev/null +++ b/src/views/shared/PromotionalCardView.ts @@ -0,0 +1,166 @@ +import * as vscode from "vscode"; + +export interface PromotionalCardConfig { + title: string; + description: string; + buttonText: string; + buttonUrl: string; + backgroundColor?: string; + textColor?: string; + buttonColor?: string; + showCard?: boolean; +} + +export class PromotionalCardView { + + public static generateStyles(): string { + return ` + .promotional-card { + background-color: var(--vscode-background); + border-radius: 12px; + padding-top: 5px; + text-align: center; + } + + .promotional-content { + margin: 0 auto; + } + + .promotional-title { + font-size: 16px; + font-weight: 700; + margin-bottom: 12px; + color: var(--vscode-foreground); + line-height: 1.3; + } + + .promotional-description { + font-size: 14px; + color: rgb(140,140,140); + margin-bottom: 20px; + line-height: 1.4; + } + + .promotional-button { + background-color: rgb(0,129,225); + color: #ffffff; + border: none; + padding: 12px 32px; + font-size: 14px; + font-weight: 600; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + text-transform: none; + letter-spacing: 0.5px; + width: 100%; + } + + .promotional-button:hover { + background-color: rgb(0,129,225); + } + + .promotional-card.hidden { + display: none; + } + `; + } + + public static generateHtml(config: PromotionalCardConfig): string { + if (!config.showCard) { + return ''; + } + + const cardStyle = config.backgroundColor ? `style="background-color: ${config.backgroundColor};"` : ''; + const titleStyle = config.textColor ? `style="color: ${config.textColor};"` : ''; + const descStyle = config.textColor ? `style="color: ${config.textColor};"` : ''; + const buttonStyle = config.buttonColor ? `style="background-color: ${config.buttonColor};"` : ''; + + return ` +
+
+

${config.title}

+

${config.description}

+ +
+
`; + } + + public static generateScript(): string { + return ` + function handlePromotionalAction(url) { + if (typeof vscode !== 'undefined') { + vscode.postMessage({ + command: 'openPromotionalLink', + url: url + }); + } + } + `; + } + + public static handleMessage(message: { command: string; url?: string }): void { + if (message.command === 'openPromotionalLink' && message.url) { + vscode.env.openExternal(vscode.Uri.parse(message.url)); + } + } + + public static getAspmConfig(): PromotionalCardConfig { + return { + title: "Unlock Full Security Coverage", + description: "This feature is part of the full Checkmarx platform. Upgrade to give your organization complete application security.", + buttonText: "Learn more", + buttonUrl: "https://docs.checkmarx.com/en/34965-68743-using-the-checkmarx-vs-code-extension---checkmarx-one-results.html#UUID-f6ae9b23-44c8-fcf3-bef2-7b136b9001a1_section-idm234938984608896", + showCard: true + }; + } + + public static getSastConfig(): PromotionalCardConfig { + return { + title: "Enhanced SAST Features Available", + description: "Get advanced static analysis with AI-powered remediation, custom rules, and enterprise-grade reporting.", + buttonText: "Explore SAST+", + buttonUrl: "https://checkmarx.com/product/static-application-security-testing/", + showCard: true + }; + } + + public static getScaConfig(): PromotionalCardConfig { + return { + title: "Enhanced SCA Features Available", + description: "Get advanced open source security with license compliance, policy enforcement, and remediation guidance.", + buttonText: "Explore SCA+", + buttonUrl: "https://checkmarx.com/product/software-composition-analysis/", + showCard: true + }; + } + + public static embedInWebview( + config: PromotionalCardConfig, + existingContent: string, + position: 'top' | 'bottom' = 'top' + ): string { + const promotionalHtml = this.generateHtml(config); + const script = this.generateScript(); + + if (position === 'top') { + return ` + ${promotionalHtml} + ${existingContent} + + `; + } else { + return ` + ${existingContent} + ${promotionalHtml} + + `; + } + } +} \ No newline at end of file diff --git a/src/webview/authenticationWebview.ts b/src/webview/authenticationWebview.ts index 7f5fd999..c6b79654 100644 --- a/src/webview/authenticationWebview.ts +++ b/src/webview/authenticationWebview.ts @@ -7,6 +7,8 @@ import { WelcomeWebview } from "../welcomePage/welcomeWebview"; import { WebViewCommand } from "../commands/webViewCommand"; import { cx } from "../cx"; import { initializeMcpConfiguration, uninstallMcp } from "../services/mcpSettingsInjector"; +import { CommonCommand } from "../commands/commonCommand"; +import { commands } from "../utils/common/commands"; export class AuthenticationWebview { public static readonly viewType = "checkmarxAuth"; @@ -65,7 +67,7 @@ export class AuthenticationWebview { return; } const panel = vscode.window.createWebviewPanel( - AuthenticationWebview.viewType, + commands.astResultsPromo, "Checkmarx One Authentication", vscode.ViewColumn.One, { @@ -113,6 +115,34 @@ export class AuthenticationWebview { await this.context.globalState.update("cxFirstWelcome", true); } + private schedulePostAuth(isAiEnabled: boolean, options?: { apiKey?: string }) { + setTimeout(async () => { + try { + this._panel.dispose(); + await this.markFirstWelcomeAsShown(); + WelcomeWebview.show(this.context, isAiEnabled); + await vscode.commands.executeCommand(commands.updateCxOneAssist); + await vscode.commands.executeCommand(commands.refreshIgnoredStatusBar); + await vscode.commands.executeCommand(commands.refreshScaStatusBar); + await vscode.commands.executeCommand(commands.refreshKicsStatusBar); + await vscode.commands.executeCommand(commands.refreshRiskManagementView); + await vscode.commands.executeCommand(commands.clearKicsDiagnostics); + if (options?.apiKey) { + if (isAiEnabled) { + await initializeMcpConfiguration(options.apiKey); + } else { + await uninstallMcp(); + } + setTimeout(() => { + this._panel.webview.postMessage({ type: "clear-message-api-validation" }); + }, 500); + } + } catch (e) { + this.logs?.warn?.(`Post-auth refresh failed: ${e?.message ?? e}`); + } + }, 1000); + } + private _getWebviewContent(): string { const styleBootStrap = this.setWebUri( "media", @@ -219,7 +249,7 @@ export class AuthenticationWebview { "Yes", "Cancel" ) - .then((selection) => { + .then(async (selection) => { if (selection === "Yes") { const authService = AuthService.getInstance(this.context); authService.logout(); @@ -233,6 +263,10 @@ export class AuthenticationWebview { "Logged out successfully." ); uninstallMcp(); + await vscode.commands.executeCommand(commands.refreshIgnoredStatusBar); + await vscode.commands.executeCommand(commands.refreshScaStatusBar); + await vscode.commands.executeCommand(commands.refreshKicsStatusBar); + await vscode.commands.executeCommand(commands.refreshRiskManagementView); } }); } else if (message.command === "authenticate") { @@ -252,12 +286,11 @@ export class AuthenticationWebview { const authService = AuthService.getInstance(this.context); const token = await authService.authenticate(baseUri, tenant); const isAiEnabled = await cx.isAiMcpServerEnabled(); + const commonCommand = new CommonCommand(this.context, this.logs); + await commonCommand.executeCheckStandaloneEnabled(); + await commonCommand.executeCheckCxOneAssistEnabled(); if (token !== "") { - setTimeout(async () => { - this._panel.dispose(); - await this.markFirstWelcomeAsShown(); - WelcomeWebview.show(this.context, isAiEnabled); - }, 1000); + this.schedulePostAuth(isAiEnabled); } else { this._panel.webview.postMessage({ command: "enableAuthButton" }); @@ -281,30 +314,16 @@ export class AuthenticationWebview { return; } - // If the API Key is valid, save it in the VSCode configuration (or wherever you prefer) authService.saveToken(this.context, message.apiKey); const isAiEnabled = await cx.isAiMcpServerEnabled(); - // Sending a success message to the window + const commonCommand = new CommonCommand(this.context, this.logs); + await commonCommand.executeCheckStandaloneEnabled(); + await commonCommand.executeCheckCxOneAssistEnabled(); this._panel.webview.postMessage({ type: "validation-success", message: "API Key validated successfully!", }); - setTimeout(async () => { - - this._panel.dispose(); - await this.markFirstWelcomeAsShown(); - WelcomeWebview.show(this.context, isAiEnabled); - if (isAiEnabled) { - await initializeMcpConfiguration(message.apiKey); - } else { - await uninstallMcp(); - } - setTimeout(() => { - this._panel.webview.postMessage({ - type: "clear-message-api-validation", - }); - }, 500); - }, 1000); + this.schedulePostAuth(isAiEnabled, { apiKey: message.apiKey }); } } catch (error) { this._panel.webview.postMessage({ command: "enableAuthButton" });