Skip to content

Commit

Permalink
CM-42035 - Add AI remediations for IaC and SAST (#120)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarshalX authored Dec 11, 2024
1 parent 2ef505d commit 28787d7
Show file tree
Hide file tree
Showing 31 changed files with 539 additions and 93 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -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"
}
}
4 changes: 2 additions & 2 deletions src/cli/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions src/cli/models/ai-remediation-result.ts
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 0 additions & 13 deletions src/cli/models/auth-check-result.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/cli/models/scan-result/detection-base.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
1 change: 1 addition & 0 deletions src/cli/models/scan-result/iac/iac-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/cli/models/scan-result/sast/sast-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/cli/models/scan-result/sca/sca-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/cli/models/scan-result/secret/secret-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions src/cli/models/status-result.ts
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 0 additions & 4 deletions src/cli/models/version-result.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/services/cli-download-service.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
72 changes: 40 additions & 32 deletions src/services/cli-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<boolean>;
checkAuth(cancellationToken?: CancellationToken): Promise<boolean>;
syncStatus(cancellationToken?: CancellationToken): Promise<void>;
doAuth(cancellationToken?: CancellationToken): Promise<boolean>;
doIgnore(
scanType: CliScanType, ignoreType: CliIgnoreType, value: string, cancellationToken?: CancellationToken
Expand All @@ -40,6 +39,9 @@ export interface ICliService {
scanPathsSca(paths: string[], onDemand: boolean, cancellationToken: CancellationToken | undefined): Promise<void>;
scanPathsIac(paths: string[], onDemand: boolean, cancellationToken: CancellationToken | undefined): Promise<void>;
scanPathsSast(paths: string[], onDemand: boolean, cancellationToken: CancellationToken | undefined): Promise<void>;
getAiRemediation(
detectionId: string, cancellationToken: CancellationToken | undefined
): Promise<AiRemediationResultData | null>;
}

@singleton()
Expand Down Expand Up @@ -127,48 +129,32 @@ export class CliService implements ICliService {
this.showScanResultsNotification(scanType, detections.length, onDemand);
}

public async healthCheck(cancellationToken?: CancellationToken): Promise<boolean> {
public async syncStatus(cancellationToken?: CancellationToken): Promise<void> {
const result = await this.cli.executeCommand(
VersionResult, [CliCommands.Version], cancellationToken,
StatusResult, [CliCommands.Status], cancellationToken,
);
const processedResult = this.processCliResult(result);

if (isCliResultSuccess<VersionResult>(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<boolean> {
const result = await this.cli.executeCommand(
AuthCheckResult, [CliCommands.AuthCheck], cancellationToken,
);
const processedResult = this.processCliResult(result);

if (!isCliResultSuccess<AuthCheckResult>(processedResult)) {
if (!isCliResultSuccess<StatusResult>(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<boolean> {
Expand Down Expand Up @@ -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<AiRemediationResultData | null> {
const result = await this.cli.executeCommand(
AiRemediationResult, [CliCommands.AiRemediation, detectionId], cancellationToken,
);
const processedResult = this.processCliResult(result);

if (!isCliResultSuccess<AiRemediationResult>(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;
}
}
36 changes: 22 additions & 14 deletions src/services/cycode-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ 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<void>;
startAuth(): Promise<void>;
startScan(scanType: CliScanType, paths: string[], onDemand: boolean): Promise<void>;
startScanForCurrentProject(scanType: CliScanType): Promise<void>;
applyDetectionIgnore(scanType: CliScanType, ignoreType: CliIgnoreType, value: string): Promise<void>;
getAiRemediation(detectionId: string): Promise<AiRemediationResultData | null>;
}

type ProgressBar = vscode.Progress<{ message?: string; increment?: number }>;
Expand All @@ -38,17 +40,17 @@ export class CycodeService implements ICycodeService {
@inject(ExtensionServiceSymbol) private extensionService: IExtensionService,
) {}

private async withProgressBar(
private async withProgressBar<T>(
message: string,
fn: (cancellationToken: vscode.CancellationToken) => Promise<void>,
fn: (cancellationToken: vscode.CancellationToken) => Promise<T>,
options: ProgressOptions = { cancellable: true, location: vscode.ProgressLocation.Notification },
): Promise<void> {
await vscode.window.withProgress(
): Promise<T> {
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) {
Expand All @@ -58,21 +60,15 @@ export class CycodeService implements ICycodeService {
} finally {
progress.report({ increment: 100 });
}
});
}) as Promise<T>;
}

public async installCliIfNeededAndCheckAuthentication() {
await this.withProgressBar(
'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 });
}
Expand All @@ -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);
});
}

Expand Down Expand Up @@ -161,4 +157,16 @@ export class CycodeService implements ICycodeService {
{ cancellable: false, location: vscode.ProgressLocation.Window },
);
}

public async getAiRemediation(detectionId: string): Promise<AiRemediationResultData | null> {
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;
},
);
}
}
4 changes: 4 additions & 0 deletions src/services/state-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class GlobalExtensionState {
public CliHash: string | null = null;
public CliDirHashes: Record<string, string> | null = null;
public CliLastUpdateCheckedAt: number | null = null;
public IsAiLargeLanguageModelEnabled = false;
}
export type GlobalExtensionStateKey = keyof GlobalExtensionState;

Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 28787d7

Please sign in to comment.