Skip to content

Commit e00a7c4

Browse files
authored
CM-38538 - Rework scan results handling (#95)
1 parent f0e035f commit e00a7c4

File tree

18 files changed

+377
-392
lines changed

18 files changed

+377
-392
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Change Log
22

3+
## [v1.9.3]
4+
5+
- Rework scan results handling
6+
37
## [v1.9.2]
48

59
- Fix CodeLens updating
@@ -77,6 +81,8 @@
7781

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

84+
[v1.9.3]: https://github.com/cycodehq/vscode-extension/releases/tag/v1.9.3
85+
8086
[v1.9.2]: https://github.com/cycodehq/vscode-extension/releases/tag/v1.9.2
8187

8288
[v1.9.1]: https://github.com/cycodehq/vscode-extension/releases/tag/v1.9.1

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"url": "https://github.com/cycodehq/vscode-extension"
99
},
1010
"homepage": "https://cycode.com/",
11-
"version": "1.9.2",
11+
"version": "1.9.3",
1212
"publisher": "cycode",
1313
"engines": {
1414
"vscode": "^1.63.0"

src/extension.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {cycodeService} from './services/CycodeService';
3030
import {getAuthState} from './utils/auth/auth_common';
3131
import {sastScan} from './services/scanners/SastScanner';
3232
import {captureException, initSentry} from './sentry';
33+
import {refreshDiagnosticCollectionData} from './services/diagnostics/common';
3334

3435
export async function activate(context: vscode.ExtensionContext) {
3536
initSentry();
@@ -43,6 +44,12 @@ export async function activate(context: vscode.ExtensionContext) {
4344

4445
const diagnosticCollection =
4546
vscode.languages.createDiagnosticCollection(extensionName);
47+
const updateDiagnosticsOnChanges = vscode.window.onDidChangeActiveTextEditor((editor) => {
48+
if (editor) {
49+
// TODO(MarshalX): refresh only for editor.document if we will need better performance
50+
refreshDiagnosticCollectionData(diagnosticCollection);
51+
}
52+
});
4653

4754
const isAuthed = extensionContext.getGlobalState(VscodeStates.IsAuthorized);
4855
extensionContext.setContext(VscodeStates.IsAuthorized, !!isAuthed);
@@ -131,7 +138,9 @@ export async function activate(context: vscode.ExtensionContext) {
131138
});
132139

133140
// add all disposables to correctly dispose them on extension deactivating
134-
context.subscriptions.push(newStatusBar, ...commands, codeLens, quickActions, scanOnSave);
141+
context.subscriptions.push(
142+
newStatusBar, ...commands, codeLens, quickActions, scanOnSave, updateDiagnosticsOnChanges
143+
);
135144
}
136145

137146
function createTreeView(

src/providers/tree-view/utils.ts

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@ import {FileScanResult} from './provider';
44
import {SeverityFirstLetter, TreeView, TreeViewDisplayedData} from './types';
55
import {ScanType, SEVERITY_PRIORITIES} from '../../constants';
66
import {cliService} from '../../services/CliService';
7-
8-
interface RefreshTreeViewDataArgs {
9-
detections: AnyDetection[];
10-
treeView?: TreeView;
11-
scanType: ScanType;
12-
}
7+
import {scanResultsService} from '../../services/ScanResultsService';
138

149
interface ValueItem {
1510
fullFilePath: string;
@@ -20,25 +15,21 @@ type SeverityCounted = { [severity: string]: number };
2015

2116
const VSCODE_ENTRY_LINE_NUMBER = 1;
2217

23-
export function refreshTreeViewData(
24-
args: RefreshTreeViewDataArgs
25-
): void {
26-
const {detections, treeView, scanType} = args;
27-
if (treeView === undefined) {
28-
return;
29-
}
30-
18+
export const refreshTreeViewData = (
19+
scanType: ScanType, treeView: TreeView
20+
) => {
3121
const projectRoot = cliService.getProjectRootDirectory();
22+
const detections = scanResultsService.getDetections(scanType);
3223

33-
const {provider} = treeView;
3424
const affectedFiles: FileScanResult[] = [];
3525
const detectionsMapped = mapDetectionsByFileName(detections, scanType);
3626
detectionsMapped.forEach((vulnerabilities, fullFilePath) => {
3727
const projectRelativePath = path.relative(projectRoot, fullFilePath);
3828
affectedFiles.push(new FileScanResult(projectRelativePath, fullFilePath, vulnerabilities));
3929
});
40-
provider.refresh(affectedFiles, scanType);
41-
}
30+
31+
treeView.provider.refresh(affectedFiles, scanType);
32+
};
4233

4334
const _getSecretValueItem = (detection: SecretDetection): ValueItem => {
4435
const {type, detection_details, severity} = detection;
@@ -108,10 +99,10 @@ const _getSastValueItem = (detection: SastDetection): ValueItem => {
10899
return {fullFilePath: file_path, data: valueItem};
109100
};
110101

111-
function mapDetectionsByFileName(
102+
const mapDetectionsByFileName = (
112103
detections: AnyDetection[],
113104
scanType: ScanType,
114-
): Map<string, TreeViewDisplayedData[]> {
105+
): Map<string, TreeViewDisplayedData[]> => {
115106
const resultMap: Map<string, TreeViewDisplayedData[]> = new Map();
116107

117108
detections.forEach((detection) => {
@@ -139,9 +130,9 @@ function mapDetectionsByFileName(
139130
});
140131

141132
return resultMap;
142-
}
133+
};
143134

144-
function mapSeverityToFirstLetter(severity: string): SeverityFirstLetter {
135+
const mapSeverityToFirstLetter = (severity: string): SeverityFirstLetter => {
145136
switch (severity.toLowerCase()) {
146137
case 'info':
147138
return SeverityFirstLetter.Info;
@@ -158,7 +149,7 @@ function mapSeverityToFirstLetter(severity: string): SeverityFirstLetter {
158149
`Supplied unsupported severity ${severity}, can not map to severity first letter`
159150
);
160151
}
161-
}
152+
};
162153

163154
export const mapScanResultsToSeverityStatsString = (scanResults: FileScanResult[]): string => {
164155
const severityToCount: SeverityCounted = {};

src/services/ScanResultsService.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,29 @@ interface ScanResult {
3131

3232
type LocalStorage = Record<string, ScanResult>;
3333

34+
const _slowDeepClone = (obj: any): any => {
35+
// TODO(MarshalX): move to faster approauch if the performance is critical
36+
return JSON.parse(JSON.stringify(obj));
37+
};
38+
3439
class ScanResultsService {
40+
// We are returning cloned objects to prevent mutations in the storage.
41+
// The mutations of detections itself happen, for example, for enriching detections for rendering violation card.
42+
// But not mutated detections are used to create diagnostics, tree view, etc.
43+
3544
public getDetectionById(detectionId: string): ScanResult | undefined {
3645
const detections = getWorkspaceState(getDetectionsKey()) as LocalStorage;
37-
return detections[detectionId] as ScanResult | undefined;
46+
return _slowDeepClone(detections[detectionId]) as ScanResult | undefined;
3847
}
3948

4049
public getDetections(scanType: ScanType): AnyDetection[] {
4150
const scanTypeKey = getScanTypeKey(scanType);
42-
return getWorkspaceState(scanTypeKey) as AnyDetection[] || [];
51+
const detections = getWorkspaceState(scanTypeKey) as AnyDetection[] || [];
52+
return _slowDeepClone(detections);
53+
}
54+
55+
public clearDetections(scanType: ScanType): void {
56+
updateWorkspaceState(getScanTypeKey(scanType), []);
4357
}
4458

4559
public saveDetections(scanType: ScanType, detections: AnyDetection[]): void {
@@ -48,12 +62,16 @@ class ScanResultsService {
4862
});
4963
}
5064

51-
public saveDetection(scanType: ScanType, detection: AnyDetection): void {
52-
const scanTypeKey = getScanTypeKey(scanType);
65+
public setDetections(scanType: ScanType, detections: AnyDetection[]): void {
66+
// TODO(MarshalX): smart merge with existing detections will be cool someday
67+
this.clearDetections(scanType);
68+
this.saveDetections(scanType, detections);
69+
}
5370

54-
const scanTypeDetections = getWorkspaceState(scanTypeKey) as AnyDetection[] || [];
71+
public saveDetection(scanType: ScanType, detection: AnyDetection): void {
72+
const scanTypeDetections = this.getDetections(scanType);
5573
scanTypeDetections.push(detection);
56-
updateWorkspaceState(scanTypeKey, scanTypeDetections);
74+
updateWorkspaceState(getScanTypeKey(scanType), scanTypeDetections);
5775

5876
const detectionsKey = getDetectionsKey();
5977
const detections = getWorkspaceState(detectionsKey) as LocalStorage;
@@ -71,7 +89,7 @@ class ScanResultsService {
7189
updateWorkspaceState(detectionsKey, {});
7290

7391
for (const scanType of Object.values(ScanType)) {
74-
updateWorkspaceState(getScanTypeKey(scanType), []);
92+
this.clearDetections(scanType);
7593
}
7694
}
7795
}

src/services/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function auth(params: CommandParams) {
3636
handleAuthStatus(exitCode, result, stderr);
3737
} catch (error) {
3838
captureException(error);
39-
extensionOutput.error('Error while creating scan: ' + error);
39+
extensionOutput.error('Error while authing: ' + error);
4040
onAuthFailure();
4141
}
4242
}

src/services/common.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {onAuthFailure} from '../utils/auth/auth_common';
66
import {getHasDetectionState, VscodeStates} from '../utils/states';
77
import {ProgressBar} from '../cli-wrapper/types';
88
import {DIAGNOSTIC_CODE_SEPARATOR, ScanType} from '../constants';
9-
import {AnyDetection} from '../types/detection';
9+
import {scanResultsService} from './ScanResultsService';
1010

1111
const _cliBadAuthMessageId = 'client id needed';
1212
const _cliBadAuthMessageSecret = 'client secret needed';
@@ -117,7 +117,8 @@ const updateHasDetectionState = (scanType: ScanType, value: boolean) => {
117117
setContext(VscodeStates.HasDetections, hasAnyDetections);
118118
};
119119

120-
export const updateDetectionState = (scanType: ScanType, detections: AnyDetection[]) => {
120+
export const updateDetectionState = (scanType: ScanType) => {
121+
const detections = scanResultsService.getDetections(scanType);
121122
const hasDetections = detections.length > 0;
122123

123124
updateHasDetectionState(scanType, hasDetections);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as path from 'path';
2+
import * as vscode from 'vscode';
3+
import {IacDetection} from '../../types/detection';
4+
import {extensionId} from '../../utils/texts';
5+
import {DiagnosticCode} from '../common';
6+
import {ScanType} from '../../constants';
7+
import {calculateUniqueDetectionId} from '../ScanResultsService';
8+
import {FileDiagnostics} from './types';
9+
10+
export const createDiagnostics = async (
11+
detections: IacDetection[],
12+
): Promise<FileDiagnostics> => {
13+
const result: FileDiagnostics = {};
14+
15+
for (const detection of detections) {
16+
const {detection_details} = detection;
17+
18+
const documentPath = detection_details.file_name;
19+
const documentUri = vscode.Uri.file(documentPath);
20+
const document = await vscode.workspace.openTextDocument(documentUri);
21+
22+
let message = `Severity: ${detection.severity}\n`;
23+
message += `Rule: ${detection.message}\n`;
24+
25+
message += `IaC Provider: ${detection_details.infra_provider}\n`;
26+
27+
const fileName = path.basename(detection_details.file_name);
28+
message += `In file: ${fileName}\n`;
29+
30+
const diagnostic = new vscode.Diagnostic(
31+
document.lineAt(detection_details.line_in_file - 1).range,
32+
message,
33+
vscode.DiagnosticSeverity.Error
34+
);
35+
36+
diagnostic.source = extensionId;
37+
diagnostic.code = new DiagnosticCode(ScanType.Iac, calculateUniqueDetectionId(detection)).toString();
38+
39+
result[documentPath] = result[documentPath] || [];
40+
result[documentPath].push(diagnostic);
41+
}
42+
43+
return result;
44+
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as vscode from 'vscode';
2+
import {SastDetection} from '../../types/detection';
3+
import {extensionId} from '../../utils/texts';
4+
import {DiagnosticCode} from '../common';
5+
import {ScanType} from '../../constants';
6+
import {calculateUniqueDetectionId} from '../ScanResultsService';
7+
import {FileDiagnostics} from './types';
8+
9+
export const createDiagnostics = async (
10+
detections: SastDetection[],
11+
): Promise<FileDiagnostics> => {
12+
const result: FileDiagnostics = {};
13+
14+
for (const detection of detections) {
15+
const {detection_details} = detection;
16+
17+
const documentPath = detection_details.file_path;
18+
const documentUri = vscode.Uri.file(documentPath);
19+
const document = await vscode.workspace.openTextDocument(documentUri);
20+
21+
let message = `Severity: ${detection.severity}\n`;
22+
message += `Rule: ${detection.detection_details.policy_display_name}\n`;
23+
message += `In file: ${detection.detection_details.file_name}\n`;
24+
25+
const diagnostic = new vscode.Diagnostic(
26+
document.lineAt(detection_details.line_in_file - 1).range,
27+
message,
28+
vscode.DiagnosticSeverity.Error
29+
);
30+
31+
diagnostic.source = extensionId;
32+
diagnostic.code = new DiagnosticCode(ScanType.Sast, calculateUniqueDetectionId(detection)).toString();
33+
34+
result[documentPath] = result[documentPath] || [];
35+
result[documentPath].push(diagnostic);
36+
}
37+
38+
return result;
39+
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as path from 'path';
2+
import * as vscode from 'vscode';
3+
import {ScaDetection} from '../../types/detection';
4+
import {getPackageFileForLockFile, isSupportedLockFile, ScanType} from '../../constants';
5+
import {extensionId} from '../../utils/texts';
6+
import {DiagnosticCode} from '../common';
7+
import {calculateUniqueDetectionId} from '../ScanResultsService';
8+
import {FileDiagnostics} from './types';
9+
10+
export const createDiagnostics = async (
11+
detections: ScaDetection[]
12+
): Promise<FileDiagnostics> => {
13+
const result: FileDiagnostics = {};
14+
15+
for (const detection of detections) {
16+
const {detection_details} = detection;
17+
const file_name = detection_details.file_name;
18+
const uri = vscode.Uri.file(file_name);
19+
const document = await vscode.workspace.openTextDocument(uri);
20+
21+
let message = `Severity: ${detection.severity}\n`;
22+
message += `${detection.message}\n`;
23+
if (detection_details.alert?.first_patched_version) {
24+
message += `First patched version: ${detection_details.alert?.first_patched_version}\n`;
25+
}
26+
27+
if (isSupportedLockFile(file_name)) {
28+
const packageFileName = getPackageFileForLockFile(path.basename(file_name));
29+
message += `\n\nAvoid manual packages upgrades in lock files.
30+
Update the ${packageFileName} file and re-generate the lock file.`;
31+
}
32+
33+
const diagnostic = new vscode.Diagnostic(
34+
// BE of SCA counts lines from 1, while VSCode counts from 0
35+
document.lineAt(detection_details.line_in_file - 1).range,
36+
message,
37+
vscode.DiagnosticSeverity.Error
38+
);
39+
40+
diagnostic.source = extensionId;
41+
diagnostic.code = new DiagnosticCode(ScanType.Sca, calculateUniqueDetectionId(detection)).toString();
42+
43+
result[file_name] = result[file_name] || [];
44+
result[file_name].push(diagnostic);
45+
}
46+
47+
return result;
48+
};

0 commit comments

Comments
 (0)