Skip to content

Commit 28787d7

Browse files
authored
CM-42035 - Add AI remediations for IaC and SAST (#120)
1 parent 2ef505d commit 28787d7

31 files changed

+539
-93
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## [Unreleased]
44

5+
## [v1.13.0]
6+
7+
- Add AI remediations for IaC and SAST
8+
- Add code highlighting for Violation Cards
9+
510
## [v1.12.0]
611

712
- Add support for Swift Package Manager in SCA
@@ -119,6 +124,8 @@
119124

120125
The first stable release with the support of Secrets, SCA, TreeView, Violation Card, and more.
121126

127+
[v1.13.0]: https://github.com/cycodehq/vscode-extension/releases/tag/v1.13.0
128+
122129
[v1.12.0]: https://github.com/cycodehq/vscode-extension/releases/tag/v1.12.0
123130

124131
[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
163170

164171
[v1.0.0]: https://github.com/cycodehq/vscode-extension/releases/tag/v1.0.0
165172

166-
[Unreleased]: https://github.com/cycodehq/vscode-extension/compare/v1.12.0...HEAD
173+
[Unreleased]: https://github.com/cycodehq/vscode-extension/compare/v1.13.0...HEAD

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cycode",
33
"displayName": "Cycode",
4-
"version": "1.12.0",
4+
"version": "1.13.0",
55
"publisher": "cycode",
66
"description": "Boost security in your dev lifecycle via SAST, SCA, Secrets & IaC scanning.",
77
"repository": {
@@ -295,6 +295,7 @@
295295
"semver": "7.5.4",
296296
"shelljs": "0.8.5",
297297
"showdown": "^2.1.0",
298+
"showdown-highlight": "^3.1.0",
298299
"tsyringe": "^4.8.0"
299300
}
300301
}

src/cli/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ export enum CliCommands {
44
Path = 'path',
55
Scan = 'scan',
66
Auth = 'auth',
7-
AuthCheck = 'auth check',
87
Ignore = 'ignore',
9-
Version = 'version',
8+
Status = 'status',
9+
AiRemediation = 'ai_remediation',
1010
}
1111

1212
export enum CommandParameters {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Type } from 'class-transformer';
2+
3+
export class AiRemediationResultData {
4+
remediation: string;
5+
isFixAvailable: boolean;
6+
}
7+
8+
export class AiRemediationResult {
9+
result: boolean;
10+
message: string;
11+
@Type(() => AiRemediationResultData)
12+
data?: AiRemediationResultData;
13+
}

src/cli/models/auth-check-result.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/cli/models/scan-result/detection-base.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ScanDetectionDetailsBase } from './scan-detection-details-base';
22

33
export abstract class DetectionBase {
4+
public abstract id: string;
45
public abstract severity: string;
56
public abstract detectionDetails: ScanDetectionDetailsBase;
67

src/cli/models/scan-result/iac/iac-detection.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Type } from 'class-transformer';
33
import { DetectionBase } from '../detection-base';
44

55
export class IacDetection extends DetectionBase {
6+
id: string;
67
message: string;
78
@Type(() => IacDetectionDetails)
89
detectionDetails: IacDetectionDetails;

src/cli/models/scan-result/sast/sast-detection.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { SastDetectionDetails } from './sast-detection-details';
33
import { DetectionBase } from '../detection-base';
44

55
export class SastDetection extends DetectionBase {
6+
id: string;
67
message: string;
78
@Type(() => SastDetectionDetails)
89
detectionDetails: SastDetectionDetails;

src/cli/models/scan-result/sca/sca-detection.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { DetectionBase } from '../detection-base';
33
import { Type } from 'class-transformer';
44

55
export class ScaDetection extends DetectionBase {
6+
id: string;
67
message: string;
78
@Type(() => ScaDetectionDetails)
89
detectionDetails: ScaDetectionDetails;

src/cli/models/scan-result/secret/secret-detection.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Type } from 'class-transformer';
55
const IDE_ENTRY_LINE_NUMBER = 1;
66

77
export class SecretDetection extends DetectionBase {
8+
id: string;
89
message: string;
910

1011
@Type(() => SecretDetectionDetails)

src/cli/models/status-result.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Type } from 'class-transformer';
2+
3+
export class SupportedModulesStatus {
4+
// TODO(MarshalX): respect enabled/disabled scanning modules
5+
secretScanning: boolean;
6+
scaScanning: boolean;
7+
iacScanning: boolean;
8+
sastScanning: boolean;
9+
aiLargeLanguageModel: boolean;
10+
}
11+
12+
export class StatusResult {
13+
program: string;
14+
version: string;
15+
isAuthenticated: boolean;
16+
userId: string | null;
17+
tenantId: string | null;
18+
@Type(() => SupportedModulesStatus)
19+
supportedModules: SupportedModulesStatus;
20+
}

src/cli/models/version-result.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export const getScanTypeDisplayName = (scanType: string): string => {
128128
return _SCAN_TYPE_TO_DISPLAY_NAME[scanType];
129129
};
130130

131-
export const REQUIRED_CLI_VERSION = '2.0.0';
131+
export const REQUIRED_CLI_VERSION = '2.1.0';
132132

133133
export const CLI_GITHUB = {
134134
OWNER: 'cycodehq',

src/services/cli-download-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
3-
import * as decompress from 'decompress';
3+
import decompress from 'decompress';
44
import { config } from '../utils/config';
55
import { GitHubRelease, GitHubReleaseAsset, IGithubReleaseService } from './github-release-service';
66
import {

src/services/cli-service.ts

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ import { CliWrapper } from '../cli/cli-wrapper';
99
import { CliResult, isCliResultError, isCliResultPanic, isCliResultSuccess } from '../cli/models/cli-result';
1010
import { ExitCode } from '../cli/exit-code';
1111
import { ScanResultBase } from '../cli/models/scan-result/scan-result-base';
12-
import { VersionResult } from '../cli/models/version-result';
1312
import { CliCommands, CommandParameters } from '../cli/constants';
14-
import { AuthCheckResult } from '../cli/models/auth-check-result';
1513
import { AuthResult } from '../cli/models/auth-result';
1614
import { getScanTypeDisplayName } from '../constants';
1715
import { ClassConstructor } from 'class-transformer';
@@ -27,11 +25,12 @@ import { IacScanResult } from '../cli/models/scan-result/iac/iac-scan-result';
2725
import { DetectionBase } from '../cli/models/scan-result/detection-base';
2826
import { CliIgnoreType } from '../cli/models/cli-ignore-type';
2927
import { CliScanType } from '../cli/models/cli-scan-type';
28+
import { StatusResult } from '../cli/models/status-result';
29+
import { AiRemediationResult, AiRemediationResultData } from '../cli/models/ai-remediation-result';
3030

3131
export interface ICliService {
3232
getProjectRootDirectory(): string | undefined; // TODO REMOVE
33-
healthCheck(cancellationToken?: CancellationToken): Promise<boolean>;
34-
checkAuth(cancellationToken?: CancellationToken): Promise<boolean>;
33+
syncStatus(cancellationToken?: CancellationToken): Promise<void>;
3534
doAuth(cancellationToken?: CancellationToken): Promise<boolean>;
3635
doIgnore(
3736
scanType: CliScanType, ignoreType: CliIgnoreType, value: string, cancellationToken?: CancellationToken
@@ -40,6 +39,9 @@ export interface ICliService {
4039
scanPathsSca(paths: string[], onDemand: boolean, cancellationToken: CancellationToken | undefined): Promise<void>;
4140
scanPathsIac(paths: string[], onDemand: boolean, cancellationToken: CancellationToken | undefined): Promise<void>;
4241
scanPathsSast(paths: string[], onDemand: boolean, cancellationToken: CancellationToken | undefined): Promise<void>;
42+
getAiRemediation(
43+
detectionId: string, cancellationToken: CancellationToken | undefined
44+
): Promise<AiRemediationResultData | null>;
4345
}
4446

4547
@singleton()
@@ -127,48 +129,32 @@ export class CliService implements ICliService {
127129
this.showScanResultsNotification(scanType, detections.length, onDemand);
128130
}
129131

130-
public async healthCheck(cancellationToken?: CancellationToken): Promise<boolean> {
132+
public async syncStatus(cancellationToken?: CancellationToken): Promise<void> {
131133
const result = await this.cli.executeCommand(
132-
VersionResult, [CliCommands.Version], cancellationToken,
134+
StatusResult, [CliCommands.Status], cancellationToken,
133135
);
134136
const processedResult = this.processCliResult(result);
135137

136-
if (isCliResultSuccess<VersionResult>(processedResult)) {
137-
this.state.CliInstalled = true;
138-
this.state.CliVer = processedResult.result.version;
139-
this.stateService.save();
140-
return true;
141-
}
142-
143-
this.resetPluginCLiStateIfNeeded(result);
144-
return false;
145-
}
146-
147-
public async checkAuth(cancellationToken?: CancellationToken): Promise<boolean> {
148-
const result = await this.cli.executeCommand(
149-
AuthCheckResult, [CliCommands.AuthCheck], cancellationToken,
150-
);
151-
const processedResult = this.processCliResult(result);
152-
153-
if (!isCliResultSuccess<AuthCheckResult>(processedResult)) {
138+
if (!isCliResultSuccess<StatusResult>(processedResult)) {
154139
this.resetPluginCLiStateIfNeeded(result);
155-
return false;
140+
return;
156141
}
157142

158143
this.state.CliInstalled = true;
159-
this.state.CliAuthed = processedResult.result.result;
144+
this.state.CliVer = processedResult.result.version;
145+
this.state.CliAuthed = processedResult.result.isAuthenticated;
146+
this.state.IsAiLargeLanguageModelEnabled = processedResult.result.supportedModules.aiLargeLanguageModel;
160147
this.stateService.save();
161148

162149
if (!this.state.CliAuthed) {
163150
this.showErrorNotification('You are not authenticated in Cycode. Please authenticate');
151+
} else {
152+
if (processedResult.result.userId && processedResult.result.tenantId) {
153+
setSentryUser(processedResult.result.userId, processedResult.result.tenantId);
154+
}
164155
}
165156

166-
const sentryData = processedResult.result.data;
167-
if (sentryData) {
168-
setSentryUser(sentryData.userId, sentryData.tenantId);
169-
}
170-
171-
return this.state.CliAuthed;
157+
return;
172158
}
173159

174160
public async doAuth(cancellationToken?: CancellationToken): Promise<boolean> {
@@ -313,4 +299,26 @@ export class CliService implements ICliService {
313299

314300
await this.processCliScanResult(CliScanType.Sast, results.result.detections, onDemand);
315301
}
302+
303+
public async getAiRemediation(
304+
detectionId: string, cancellationToken: CancellationToken | undefined = undefined,
305+
): Promise<AiRemediationResultData | null> {
306+
const result = await this.cli.executeCommand(
307+
AiRemediationResult, [CliCommands.AiRemediation, detectionId], cancellationToken,
308+
);
309+
const processedResult = this.processCliResult(result);
310+
311+
if (!isCliResultSuccess<AiRemediationResult>(processedResult)) {
312+
this.logger.warn(`Failed to generate AI remediation for the detection ID ${detectionId}`);
313+
return null;
314+
}
315+
316+
if (!processedResult.result.result || processedResult.result.data?.remediation === undefined) {
317+
this.logger.warn(`AI remediation result is not available for the detection ID ${detectionId}`);
318+
this.showErrorNotification('AI remediation is not available for this detection');
319+
return null;
320+
}
321+
322+
return processedResult.result.data;
323+
}
316324
}

src/services/cycode-service.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ import { CliIgnoreType } from '../cli/models/cli-ignore-type';
1717
import { IScanResultsService } from './scan-results-service';
1818
import { IExtensionService } from './extension-service';
1919
import { CliScanType } from '../cli/models/cli-scan-type';
20+
import { AiRemediationResultData } from '../cli/models/ai-remediation-result';
2021

2122
export interface ICycodeService {
2223
installCliIfNeededAndCheckAuthentication(): Promise<void>;
2324
startAuth(): Promise<void>;
2425
startScan(scanType: CliScanType, paths: string[], onDemand: boolean): Promise<void>;
2526
startScanForCurrentProject(scanType: CliScanType): Promise<void>;
2627
applyDetectionIgnore(scanType: CliScanType, ignoreType: CliIgnoreType, value: string): Promise<void>;
28+
getAiRemediation(detectionId: string): Promise<AiRemediationResultData | null>;
2729
}
2830

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

41-
private async withProgressBar(
43+
private async withProgressBar<T>(
4244
message: string,
43-
fn: (cancellationToken: vscode.CancellationToken) => Promise<void>,
45+
fn: (cancellationToken: vscode.CancellationToken) => Promise<T>,
4446
options: ProgressOptions = { cancellable: true, location: vscode.ProgressLocation.Notification },
45-
): Promise<void> {
46-
await vscode.window.withProgress(
47+
): Promise<T> {
48+
return vscode.window.withProgress(
4749
options,
4850
async (progress: ProgressBar, cancellationToken: vscode.CancellationToken) => {
4951
try {
5052
progress.report({ message });
51-
await fn(cancellationToken);
53+
return await fn(cancellationToken);
5254
} catch (error: unknown) {
5355
captureException(error);
5456
if (error instanceof Error) {
@@ -58,21 +60,15 @@ export class CycodeService implements ICycodeService {
5860
} finally {
5961
progress.report({ increment: 100 });
6062
}
61-
});
63+
}) as Promise<T>;
6264
}
6365

6466
public async installCliIfNeededAndCheckAuthentication() {
6567
await this.withProgressBar(
6668
'Cycode is loading...',
6769
async (cancellationToken: vscode.CancellationToken) => {
6870
await this.cliDownloadService.initCli();
69-
70-
/*
71-
* required to know CLI version.
72-
* we don't have a universal command that will cover the auth state and CLI version yet
73-
*/
74-
await this.cliService.healthCheck(cancellationToken);
75-
await this.cliService.checkAuth(cancellationToken);
71+
await this.cliService.syncStatus(cancellationToken);
7672
},
7773
{ cancellable: false, location: vscode.ProgressLocation.Window });
7874
}
@@ -82,7 +78,7 @@ export class CycodeService implements ICycodeService {
8278
'Authenticating to Cycode...',
8379
async (cancellationToken: vscode.CancellationToken) => {
8480
await this.cliService.doAuth(cancellationToken);
85-
await this.cliService.checkAuth(cancellationToken);
81+
await this.cliService.syncStatus(cancellationToken);
8682
});
8783
}
8884

@@ -161,4 +157,16 @@ export class CycodeService implements ICycodeService {
161157
{ cancellable: false, location: vscode.ProgressLocation.Window },
162158
);
163159
}
160+
161+
public async getAiRemediation(detectionId: string): Promise<AiRemediationResultData | null> {
162+
return await this.withProgressBar(
163+
'Cycode is generating AI remediation...',
164+
async (cancellationToken: vscode.CancellationToken) => {
165+
this.logger.debug(`[AI REMEDIATION] Start generating remediation for ${detectionId}`);
166+
const remediation = await this.cliService.getAiRemediation(detectionId, cancellationToken);
167+
this.logger.debug(`[AI REMEDIATION] Finish generating remediation for ${detectionId}`);
168+
return remediation;
169+
},
170+
);
171+
}
164172
}

src/services/state-service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export class GlobalExtensionState {
1212
public CliHash: string | null = null;
1313
public CliDirHashes: Record<string, string> | null = null;
1414
public CliLastUpdateCheckedAt: number | null = null;
15+
public IsAiLargeLanguageModelEnabled = false;
1516
}
1617
export type GlobalExtensionStateKey = keyof GlobalExtensionState;
1718

@@ -160,6 +161,9 @@ export class StateService implements IStateService {
160161
if (extensionState.CliLastUpdateCheckedAt !== undefined) (
161162
this._globalState.CliLastUpdateCheckedAt = extensionState.CliLastUpdateCheckedAt
162163
);
164+
if (extensionState.IsAiLargeLanguageModelEnabled !== undefined) (
165+
this._globalState.IsAiLargeLanguageModelEnabled = extensionState.IsAiLargeLanguageModelEnabled
166+
);
163167
}
164168

165169
// eslint-disable-next-line @typescript-eslint/no-unused-vars

0 commit comments

Comments
 (0)