diff --git a/CHANGELOG.md b/CHANGELOG.md index 04b3d38..0e50cdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +## [v1.13.0] + +- Add AI remediations for IaC and SAST +- Add code highlighting for Violation Cards + ## [v1.12.0] - Add support for Swift Package Manager in SCA @@ -119,6 +124,8 @@ The first stable release with the support of Secrets, SCA, TreeView, Violation Card, and more. +[v1.13.0]: https://github.com/cycodehq/vscode-extension/releases/tag/v1.13.0 + [v1.12.0]: https://github.com/cycodehq/vscode-extension/releases/tag/v1.12.0 [v1.11.2]: https://github.com/cycodehq/vscode-extension/releases/tag/v1.11.2 @@ -163,4 +170,4 @@ The first stable release with the support of Secrets, SCA, TreeView, Violation C [v1.0.0]: https://github.com/cycodehq/vscode-extension/releases/tag/v1.0.0 -[Unreleased]: https://github.com/cycodehq/vscode-extension/compare/v1.12.0...HEAD +[Unreleased]: https://github.com/cycodehq/vscode-extension/compare/v1.13.0...HEAD diff --git a/package.json b/package.json index 39c8227..45a546f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cycode", "displayName": "Cycode", - "version": "1.12.0", + "version": "1.13.0", "publisher": "cycode", "description": "Boost security in your dev lifecycle via SAST, SCA, Secrets & IaC scanning.", "repository": { @@ -295,6 +295,7 @@ "semver": "7.5.4", "shelljs": "0.8.5", "showdown": "^2.1.0", + "showdown-highlight": "^3.1.0", "tsyringe": "^4.8.0" } } diff --git a/src/cli/constants.ts b/src/cli/constants.ts index 2ad4bfd..b0c41d6 100644 --- a/src/cli/constants.ts +++ b/src/cli/constants.ts @@ -4,9 +4,9 @@ export enum CliCommands { Path = 'path', Scan = 'scan', Auth = 'auth', - AuthCheck = 'auth check', Ignore = 'ignore', - Version = 'version', + Status = 'status', + AiRemediation = 'ai_remediation', } export enum CommandParameters { diff --git a/src/cli/models/ai-remediation-result.ts b/src/cli/models/ai-remediation-result.ts new file mode 100644 index 0000000..45d5962 --- /dev/null +++ b/src/cli/models/ai-remediation-result.ts @@ -0,0 +1,13 @@ +import { Type } from 'class-transformer'; + +export class AiRemediationResultData { + remediation: string; + isFixAvailable: boolean; +} + +export class AiRemediationResult { + result: boolean; + message: string; + @Type(() => AiRemediationResultData) + data?: AiRemediationResultData; +} diff --git a/src/cli/models/auth-check-result.ts b/src/cli/models/auth-check-result.ts deleted file mode 100644 index 47a526f..0000000 --- a/src/cli/models/auth-check-result.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Type } from 'class-transformer'; - -export class AuthCheckResultData { - userId: string; - tenantId: string; -} - -export class AuthCheckResult { - result: boolean; - message: string; - @Type(() => AuthCheckResultData) - data?: AuthCheckResultData; -} diff --git a/src/cli/models/scan-result/detection-base.ts b/src/cli/models/scan-result/detection-base.ts index a7e23c2..eb3560b 100644 --- a/src/cli/models/scan-result/detection-base.ts +++ b/src/cli/models/scan-result/detection-base.ts @@ -1,6 +1,7 @@ import { ScanDetectionDetailsBase } from './scan-detection-details-base'; export abstract class DetectionBase { + public abstract id: string; public abstract severity: string; public abstract detectionDetails: ScanDetectionDetailsBase; diff --git a/src/cli/models/scan-result/iac/iac-detection.ts b/src/cli/models/scan-result/iac/iac-detection.ts index 40f4158..22d9b5a 100644 --- a/src/cli/models/scan-result/iac/iac-detection.ts +++ b/src/cli/models/scan-result/iac/iac-detection.ts @@ -3,6 +3,7 @@ import { Type } from 'class-transformer'; import { DetectionBase } from '../detection-base'; export class IacDetection extends DetectionBase { + id: string; message: string; @Type(() => IacDetectionDetails) detectionDetails: IacDetectionDetails; diff --git a/src/cli/models/scan-result/sast/sast-detection.ts b/src/cli/models/scan-result/sast/sast-detection.ts index d29cac9..6509153 100644 --- a/src/cli/models/scan-result/sast/sast-detection.ts +++ b/src/cli/models/scan-result/sast/sast-detection.ts @@ -3,6 +3,7 @@ import { SastDetectionDetails } from './sast-detection-details'; import { DetectionBase } from '../detection-base'; export class SastDetection extends DetectionBase { + id: string; message: string; @Type(() => SastDetectionDetails) detectionDetails: SastDetectionDetails; diff --git a/src/cli/models/scan-result/sca/sca-detection.ts b/src/cli/models/scan-result/sca/sca-detection.ts index aa98b63..25bd8a6 100644 --- a/src/cli/models/scan-result/sca/sca-detection.ts +++ b/src/cli/models/scan-result/sca/sca-detection.ts @@ -3,6 +3,7 @@ import { DetectionBase } from '../detection-base'; import { Type } from 'class-transformer'; export class ScaDetection extends DetectionBase { + id: string; message: string; @Type(() => ScaDetectionDetails) detectionDetails: ScaDetectionDetails; diff --git a/src/cli/models/scan-result/secret/secret-detection.ts b/src/cli/models/scan-result/secret/secret-detection.ts index 929f163..2e22cc1 100644 --- a/src/cli/models/scan-result/secret/secret-detection.ts +++ b/src/cli/models/scan-result/secret/secret-detection.ts @@ -5,6 +5,7 @@ import { Type } from 'class-transformer'; const IDE_ENTRY_LINE_NUMBER = 1; export class SecretDetection extends DetectionBase { + id: string; message: string; @Type(() => SecretDetectionDetails) diff --git a/src/cli/models/status-result.ts b/src/cli/models/status-result.ts new file mode 100644 index 0000000..17665ce --- /dev/null +++ b/src/cli/models/status-result.ts @@ -0,0 +1,20 @@ +import { Type } from 'class-transformer'; + +export class SupportedModulesStatus { + // TODO(MarshalX): respect enabled/disabled scanning modules + secretScanning: boolean; + scaScanning: boolean; + iacScanning: boolean; + sastScanning: boolean; + aiLargeLanguageModel: boolean; +} + +export class StatusResult { + program: string; + version: string; + isAuthenticated: boolean; + userId: string | null; + tenantId: string | null; + @Type(() => SupportedModulesStatus) + supportedModules: SupportedModulesStatus; +} diff --git a/src/cli/models/version-result.ts b/src/cli/models/version-result.ts deleted file mode 100644 index 2518c38..0000000 --- a/src/cli/models/version-result.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class VersionResult { - name: string; - version: string; -} diff --git a/src/constants.ts b/src/constants.ts index 61d3b08..ba583f5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -128,7 +128,7 @@ export const getScanTypeDisplayName = (scanType: string): string => { return _SCAN_TYPE_TO_DISPLAY_NAME[scanType]; }; -export const REQUIRED_CLI_VERSION = '2.0.0'; +export const REQUIRED_CLI_VERSION = '2.1.0'; export const CLI_GITHUB = { OWNER: 'cycodehq', diff --git a/src/services/cli-download-service.ts b/src/services/cli-download-service.ts index b86608e..e1278ea 100644 --- a/src/services/cli-download-service.ts +++ b/src/services/cli-download-service.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as decompress from 'decompress'; +import decompress from 'decompress'; import { config } from '../utils/config'; import { GitHubRelease, GitHubReleaseAsset, IGithubReleaseService } from './github-release-service'; import { diff --git a/src/services/cli-service.ts b/src/services/cli-service.ts index 32f8858..044f05f 100644 --- a/src/services/cli-service.ts +++ b/src/services/cli-service.ts @@ -9,9 +9,7 @@ import { CliWrapper } from '../cli/cli-wrapper'; import { CliResult, isCliResultError, isCliResultPanic, isCliResultSuccess } from '../cli/models/cli-result'; import { ExitCode } from '../cli/exit-code'; import { ScanResultBase } from '../cli/models/scan-result/scan-result-base'; -import { VersionResult } from '../cli/models/version-result'; import { CliCommands, CommandParameters } from '../cli/constants'; -import { AuthCheckResult } from '../cli/models/auth-check-result'; import { AuthResult } from '../cli/models/auth-result'; import { getScanTypeDisplayName } from '../constants'; import { ClassConstructor } from 'class-transformer'; @@ -27,11 +25,12 @@ import { IacScanResult } from '../cli/models/scan-result/iac/iac-scan-result'; import { DetectionBase } from '../cli/models/scan-result/detection-base'; import { CliIgnoreType } from '../cli/models/cli-ignore-type'; import { CliScanType } from '../cli/models/cli-scan-type'; +import { StatusResult } from '../cli/models/status-result'; +import { AiRemediationResult, AiRemediationResultData } from '../cli/models/ai-remediation-result'; export interface ICliService { getProjectRootDirectory(): string | undefined; // TODO REMOVE - healthCheck(cancellationToken?: CancellationToken): Promise; - checkAuth(cancellationToken?: CancellationToken): Promise; + syncStatus(cancellationToken?: CancellationToken): Promise; doAuth(cancellationToken?: CancellationToken): Promise; doIgnore( scanType: CliScanType, ignoreType: CliIgnoreType, value: string, cancellationToken?: CancellationToken @@ -40,6 +39,9 @@ export interface ICliService { scanPathsSca(paths: string[], onDemand: boolean, cancellationToken: CancellationToken | undefined): Promise; scanPathsIac(paths: string[], onDemand: boolean, cancellationToken: CancellationToken | undefined): Promise; scanPathsSast(paths: string[], onDemand: boolean, cancellationToken: CancellationToken | undefined): Promise; + getAiRemediation( + detectionId: string, cancellationToken: CancellationToken | undefined + ): Promise; } @singleton() @@ -127,48 +129,32 @@ export class CliService implements ICliService { this.showScanResultsNotification(scanType, detections.length, onDemand); } - public async healthCheck(cancellationToken?: CancellationToken): Promise { + public async syncStatus(cancellationToken?: CancellationToken): Promise { const result = await this.cli.executeCommand( - VersionResult, [CliCommands.Version], cancellationToken, + StatusResult, [CliCommands.Status], cancellationToken, ); const processedResult = this.processCliResult(result); - if (isCliResultSuccess(processedResult)) { - this.state.CliInstalled = true; - this.state.CliVer = processedResult.result.version; - this.stateService.save(); - return true; - } - - this.resetPluginCLiStateIfNeeded(result); - return false; - } - - public async checkAuth(cancellationToken?: CancellationToken): Promise { - const result = await this.cli.executeCommand( - AuthCheckResult, [CliCommands.AuthCheck], cancellationToken, - ); - const processedResult = this.processCliResult(result); - - if (!isCliResultSuccess(processedResult)) { + if (!isCliResultSuccess(processedResult)) { this.resetPluginCLiStateIfNeeded(result); - return false; + return; } this.state.CliInstalled = true; - this.state.CliAuthed = processedResult.result.result; + this.state.CliVer = processedResult.result.version; + this.state.CliAuthed = processedResult.result.isAuthenticated; + this.state.IsAiLargeLanguageModelEnabled = processedResult.result.supportedModules.aiLargeLanguageModel; this.stateService.save(); if (!this.state.CliAuthed) { this.showErrorNotification('You are not authenticated in Cycode. Please authenticate'); + } else { + if (processedResult.result.userId && processedResult.result.tenantId) { + setSentryUser(processedResult.result.userId, processedResult.result.tenantId); + } } - const sentryData = processedResult.result.data; - if (sentryData) { - setSentryUser(sentryData.userId, sentryData.tenantId); - } - - return this.state.CliAuthed; + return; } public async doAuth(cancellationToken?: CancellationToken): Promise { @@ -313,4 +299,26 @@ export class CliService implements ICliService { await this.processCliScanResult(CliScanType.Sast, results.result.detections, onDemand); } + + public async getAiRemediation( + detectionId: string, cancellationToken: CancellationToken | undefined = undefined, + ): Promise { + const result = await this.cli.executeCommand( + AiRemediationResult, [CliCommands.AiRemediation, detectionId], cancellationToken, + ); + const processedResult = this.processCliResult(result); + + if (!isCliResultSuccess(processedResult)) { + this.logger.warn(`Failed to generate AI remediation for the detection ID ${detectionId}`); + return null; + } + + if (!processedResult.result.result || processedResult.result.data?.remediation === undefined) { + this.logger.warn(`AI remediation result is not available for the detection ID ${detectionId}`); + this.showErrorNotification('AI remediation is not available for this detection'); + return null; + } + + return processedResult.result.data; + } } diff --git a/src/services/cycode-service.ts b/src/services/cycode-service.ts index 33bdf70..4e05f1e 100644 --- a/src/services/cycode-service.ts +++ b/src/services/cycode-service.ts @@ -17,6 +17,7 @@ import { CliIgnoreType } from '../cli/models/cli-ignore-type'; import { IScanResultsService } from './scan-results-service'; import { IExtensionService } from './extension-service'; import { CliScanType } from '../cli/models/cli-scan-type'; +import { AiRemediationResultData } from '../cli/models/ai-remediation-result'; export interface ICycodeService { installCliIfNeededAndCheckAuthentication(): Promise; @@ -24,6 +25,7 @@ export interface ICycodeService { startScan(scanType: CliScanType, paths: string[], onDemand: boolean): Promise; startScanForCurrentProject(scanType: CliScanType): Promise; applyDetectionIgnore(scanType: CliScanType, ignoreType: CliIgnoreType, value: string): Promise; + getAiRemediation(detectionId: string): Promise; } type ProgressBar = vscode.Progress<{ message?: string; increment?: number }>; @@ -38,17 +40,17 @@ export class CycodeService implements ICycodeService { @inject(ExtensionServiceSymbol) private extensionService: IExtensionService, ) {} - private async withProgressBar( + private async withProgressBar( message: string, - fn: (cancellationToken: vscode.CancellationToken) => Promise, + fn: (cancellationToken: vscode.CancellationToken) => Promise, options: ProgressOptions = { cancellable: true, location: vscode.ProgressLocation.Notification }, - ): Promise { - await vscode.window.withProgress( + ): Promise { + return vscode.window.withProgress( options, async (progress: ProgressBar, cancellationToken: vscode.CancellationToken) => { try { progress.report({ message }); - await fn(cancellationToken); + return await fn(cancellationToken); } catch (error: unknown) { captureException(error); if (error instanceof Error) { @@ -58,7 +60,7 @@ export class CycodeService implements ICycodeService { } finally { progress.report({ increment: 100 }); } - }); + }) as Promise; } public async installCliIfNeededAndCheckAuthentication() { @@ -66,13 +68,7 @@ export class CycodeService implements ICycodeService { 'Cycode is loading...', async (cancellationToken: vscode.CancellationToken) => { await this.cliDownloadService.initCli(); - - /* - * required to know CLI version. - * we don't have a universal command that will cover the auth state and CLI version yet - */ - await this.cliService.healthCheck(cancellationToken); - await this.cliService.checkAuth(cancellationToken); + await this.cliService.syncStatus(cancellationToken); }, { cancellable: false, location: vscode.ProgressLocation.Window }); } @@ -82,7 +78,7 @@ export class CycodeService implements ICycodeService { 'Authenticating to Cycode...', async (cancellationToken: vscode.CancellationToken) => { await this.cliService.doAuth(cancellationToken); - await this.cliService.checkAuth(cancellationToken); + await this.cliService.syncStatus(cancellationToken); }); } @@ -161,4 +157,16 @@ export class CycodeService implements ICycodeService { { cancellable: false, location: vscode.ProgressLocation.Window }, ); } + + public async getAiRemediation(detectionId: string): Promise { + return await this.withProgressBar( + 'Cycode is generating AI remediation...', + async (cancellationToken: vscode.CancellationToken) => { + this.logger.debug(`[AI REMEDIATION] Start generating remediation for ${detectionId}`); + const remediation = await this.cliService.getAiRemediation(detectionId, cancellationToken); + this.logger.debug(`[AI REMEDIATION] Finish generating remediation for ${detectionId}`); + return remediation; + }, + ); + } } diff --git a/src/services/state-service.ts b/src/services/state-service.ts index fa44217..3bedddf 100644 --- a/src/services/state-service.ts +++ b/src/services/state-service.ts @@ -12,6 +12,7 @@ export class GlobalExtensionState { public CliHash: string | null = null; public CliDirHashes: Record | null = null; public CliLastUpdateCheckedAt: number | null = null; + public IsAiLargeLanguageModelEnabled = false; } export type GlobalExtensionStateKey = keyof GlobalExtensionState; @@ -160,6 +161,9 @@ export class StateService implements IStateService { if (extensionState.CliLastUpdateCheckedAt !== undefined) ( this._globalState.CliLastUpdateCheckedAt = extensionState.CliLastUpdateCheckedAt ); + if (extensionState.IsAiLargeLanguageModelEnabled !== undefined) ( + this._globalState.IsAiLargeLanguageModelEnabled = extensionState.IsAiLargeLanguageModelEnabled + ); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/ui/panels/violation/card/iac.ts b/src/ui/panels/violation/card/iac.ts index cf2fcca..12ceb3b 100644 --- a/src/ui/panels/violation/card/iac.ts +++ b/src/ui/panels/violation/card/iac.ts @@ -37,5 +37,15 @@ export default `
Cycode Guidelines
None
+ +
+
AI Remediation
+
None
+
+ + `; diff --git a/src/ui/panels/violation/card/sast.ts b/src/ui/panels/violation/card/sast.ts index 2706580..7dc9583 100644 --- a/src/ui/panels/violation/card/sast.ts +++ b/src/ui/panels/violation/card/sast.ts @@ -45,5 +45,15 @@ export default `
Cycode Guidelines
None
+ +
+
AI Remediation
+
None
+
+ + `; diff --git a/src/ui/panels/violation/content.ts b/src/ui/panels/violation/content.ts index c8a8f01..36b10d0 100644 --- a/src/ui/panels/violation/content.ts +++ b/src/ui/panels/violation/content.ts @@ -1,4 +1,7 @@ -import style from './style'; +import * as vscode from 'vscode'; +import mainStyles from './styles/main'; +import hljsGithubLightThemeStyles from './styles/hljs/github-light-default'; +import hljsGithubDarkThemeStyles from './styles/hljs/github-dark-dimmed'; import scaCard from './card/sca'; import secretCard from './card/secret'; import iacCard from './card/iac'; @@ -6,6 +9,13 @@ import sastCard from './card/sast'; import js from './js'; import { CliScanType } from '../../../cli/models/cli-scan-type'; +let isDarkTheme = vscode.window.activeColorTheme.kind !== vscode.ColorThemeKind.Light; + +vscode.window.onDidChangeActiveColorTheme((theme) => { + // TODO(MarshalX): rerender panel with new theme. Now it requires to close and open the panel + isDarkTheme = theme.kind !== vscode.ColorThemeKind.Light; +}); + export default (scanType: CliScanType) => ` @@ -15,7 +25,8 @@ export default (scanType: CliScanType) => ` Cycode: Detection Details - ${style} + ${mainStyles} + ${isDarkTheme ? hljsGithubDarkThemeStyles : hljsGithubLightThemeStyles} ${scanType == CliScanType.Sca ? scaCard : ''} ${scanType == CliScanType.Secret ? secretCard : ''} diff --git a/src/ui/panels/violation/js.ts b/src/ui/panels/violation/js.ts index ef16530..26f07e7 100644 --- a/src/ui/panels/violation/js.ts +++ b/src/ui/panels/violation/js.ts @@ -3,15 +3,26 @@ import secretRenderer from './renderer/secret'; import iacRenderer from './renderer/iac'; import sastRenderer from './renderer/sast'; import { CliScanType } from '../../../cli/models/cli-scan-type'; +import { container } from 'tsyringe'; +import { IStateService } from '../../../services/state-service'; +import { StateServiceSymbol } from '../../../symbols'; + +const isAiEnabled = () => { + const stateService = container.resolve(StateServiceSymbol); + return stateService.globalState.IsAiLargeLanguageModelEnabled; +}; export default (detectionType: CliScanType) => ` ${detectionType === CliScanType.Sca ? scaRenderer : ''} ${detectionType === CliScanType.Secret ? secretRenderer : ''} @@ -65,7 +106,7 @@ export default (detectionType: CliScanType) => ` } const updateState = () => { - vscode.setState({ severityIcons, detection }); + vscode.setState({ severityIcons, detection, uniqueDetectionId }); }; const messageHandler = event => { @@ -74,18 +115,26 @@ export default (detectionType: CliScanType) => ` if (message.uniqueDetectionId) { uniqueDetectionId = message.uniqueDetectionId; } - if (message.severityIcons) { severityIcons = message.severityIcons; - updateState(); - } else if (message.detection) { + } + if (message.detection) { detection = message.detection; - updateState(); + aiRemediation = undefined; // reset AI remediation when detection changes } + if (message.aiRemediation) { + aiRemediation = message.aiRemediation; + } + + updateState(); - if (severityIcons && detection) { + if (renderDetection && severityIcons && detection) { renderDetection(detection); } + + if (aiRemediation) { + renderAiRemediation(aiRemediation.remediation, aiRemediation.isFixAvailable); + } }; window.addEventListener('message', messageHandler); diff --git a/src/ui/panels/violation/rendered-detection.ts b/src/ui/panels/violation/rendered-detection.ts index 1bcbfab..2bdfc54 100644 --- a/src/ui/panels/violation/rendered-detection.ts +++ b/src/ui/panels/violation/rendered-detection.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import { Converter } from 'showdown'; +import showdownHighlight from 'showdown-highlight'; import { DetectionBase } from '../../../cli/models/scan-result/detection-base'; import { ScaDetection } from '../../../cli/models/scan-result/sca/sca-detection'; import { SecretDetection } from '../../../cli/models/scan-result/secret/secret-detection'; @@ -8,7 +9,17 @@ import { SastDetection } from '../../../cli/models/scan-result/sast/sast-detecti import { instanceToPlain } from 'class-transformer'; import { CliScanType } from '../../../cli/models/cli-scan-type'; -const _MARKDOWN_CONVERTER = new Converter(); +const _MARKDOWN_CONVERTER = new Converter({ + extensions: [ + showdownHighlight({ + // add the classes to the
 tag
+      pre: true,
+      // auto language detection
+      // eslint-disable-next-line camelcase
+      auto_detection: true,
+    }),
+  ],
+});
 // BE not always return markdown/html links, so we need to parse it by ourselves
 _MARKDOWN_CONVERTER.setOption('simplifiedAutoLink', true);
 _MARKDOWN_CONVERTER.setOption('openLinksInNewWindow', true); // make sure that it will open with noreferrer, etc.
@@ -34,6 +45,10 @@ export const getDetectionForRender = (detectionType: CliScanType, detection: Det
   return enrichFunction ? enrichFunction(detection) : detection;
 };
 
+export const getMarkdownForRender = (markdown: string): string => {
+  return _MARKDOWN_CONVERTER.makeHtml(markdown);
+};
+
 const _updateDetectionDetailsFieldWithHtmlIfValid = (plainDetectionDetails: Record, field: string) => {
   /*
    * a little bit hacky because of record type
@@ -48,7 +63,7 @@ const _updateDetectionDetailsFieldWithHtmlIfValid = (plainDetectionDetails: Reco
     return;
   }
 
-  plainDetectionDetails[field] = _MARKDOWN_CONVERTER.makeHtml(plainDetectionDetails[field]);
+  plainDetectionDetails[field] = getMarkdownForRender(plainDetectionDetails[field]);
 };
 
 const _getScaDetectionForRender = (detection: ScaDetection): object => {
diff --git a/src/ui/panels/violation/renderer/iac.ts b/src/ui/panels/violation/renderer/iac.ts
index 1f46c2c..76c4948 100644
--- a/src/ui/panels/violation/renderer/iac.ts
+++ b/src/ui/panels/violation/renderer/iac.ts
@@ -1,6 +1,8 @@
 export default `