Skip to content

Commit

Permalink
CM-38538 - Rework scan results handling (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarshalX authored Aug 2, 2024
1 parent f0e035f commit e00a7c4
Show file tree
Hide file tree
Showing 18 changed files with 377 additions and 392 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Change Log

## [v1.9.3]

- Rework scan results handling

## [v1.9.2]

- Fix CodeLens updating
Expand Down Expand Up @@ -77,6 +81,8 @@

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

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

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

[v1.9.1]: https://github.com/cycodehq/vscode-extension/releases/tag/v1.9.1
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"url": "https://github.com/cycodehq/vscode-extension"
},
"homepage": "https://cycode.com/",
"version": "1.9.2",
"version": "1.9.3",
"publisher": "cycode",
"engines": {
"vscode": "^1.63.0"
Expand Down
11 changes: 10 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {cycodeService} from './services/CycodeService';
import {getAuthState} from './utils/auth/auth_common';
import {sastScan} from './services/scanners/SastScanner';
import {captureException, initSentry} from './sentry';
import {refreshDiagnosticCollectionData} from './services/diagnostics/common';

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

const diagnosticCollection =
vscode.languages.createDiagnosticCollection(extensionName);
const updateDiagnosticsOnChanges = vscode.window.onDidChangeActiveTextEditor((editor) => {
if (editor) {
// TODO(MarshalX): refresh only for editor.document if we will need better performance
refreshDiagnosticCollectionData(diagnosticCollection);
}
});

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

// add all disposables to correctly dispose them on extension deactivating
context.subscriptions.push(newStatusBar, ...commands, codeLens, quickActions, scanOnSave);
context.subscriptions.push(
newStatusBar, ...commands, codeLens, quickActions, scanOnSave, updateDiagnosticsOnChanges
);
}

function createTreeView(
Expand Down
35 changes: 13 additions & 22 deletions src/providers/tree-view/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ import {FileScanResult} from './provider';
import {SeverityFirstLetter, TreeView, TreeViewDisplayedData} from './types';
import {ScanType, SEVERITY_PRIORITIES} from '../../constants';
import {cliService} from '../../services/CliService';

interface RefreshTreeViewDataArgs {
detections: AnyDetection[];
treeView?: TreeView;
scanType: ScanType;
}
import {scanResultsService} from '../../services/ScanResultsService';

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

const VSCODE_ENTRY_LINE_NUMBER = 1;

export function refreshTreeViewData(
args: RefreshTreeViewDataArgs
): void {
const {detections, treeView, scanType} = args;
if (treeView === undefined) {
return;
}

export const refreshTreeViewData = (
scanType: ScanType, treeView: TreeView
) => {
const projectRoot = cliService.getProjectRootDirectory();
const detections = scanResultsService.getDetections(scanType);

const {provider} = treeView;
const affectedFiles: FileScanResult[] = [];
const detectionsMapped = mapDetectionsByFileName(detections, scanType);
detectionsMapped.forEach((vulnerabilities, fullFilePath) => {
const projectRelativePath = path.relative(projectRoot, fullFilePath);
affectedFiles.push(new FileScanResult(projectRelativePath, fullFilePath, vulnerabilities));
});
provider.refresh(affectedFiles, scanType);
}

treeView.provider.refresh(affectedFiles, scanType);
};

const _getSecretValueItem = (detection: SecretDetection): ValueItem => {
const {type, detection_details, severity} = detection;
Expand Down Expand Up @@ -108,10 +99,10 @@ const _getSastValueItem = (detection: SastDetection): ValueItem => {
return {fullFilePath: file_path, data: valueItem};
};

function mapDetectionsByFileName(
const mapDetectionsByFileName = (
detections: AnyDetection[],
scanType: ScanType,
): Map<string, TreeViewDisplayedData[]> {
): Map<string, TreeViewDisplayedData[]> => {
const resultMap: Map<string, TreeViewDisplayedData[]> = new Map();

detections.forEach((detection) => {
Expand Down Expand Up @@ -139,9 +130,9 @@ function mapDetectionsByFileName(
});

return resultMap;
}
};

function mapSeverityToFirstLetter(severity: string): SeverityFirstLetter {
const mapSeverityToFirstLetter = (severity: string): SeverityFirstLetter => {
switch (severity.toLowerCase()) {
case 'info':
return SeverityFirstLetter.Info;
Expand All @@ -158,7 +149,7 @@ function mapSeverityToFirstLetter(severity: string): SeverityFirstLetter {
`Supplied unsupported severity ${severity}, can not map to severity first letter`
);
}
}
};

export const mapScanResultsToSeverityStatsString = (scanResults: FileScanResult[]): string => {
const severityToCount: SeverityCounted = {};
Expand Down
32 changes: 25 additions & 7 deletions src/services/ScanResultsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,29 @@ interface ScanResult {

type LocalStorage = Record<string, ScanResult>;

const _slowDeepClone = (obj: any): any => {
// TODO(MarshalX): move to faster approauch if the performance is critical
return JSON.parse(JSON.stringify(obj));
};

class ScanResultsService {
// We are returning cloned objects to prevent mutations in the storage.
// The mutations of detections itself happen, for example, for enriching detections for rendering violation card.
// But not mutated detections are used to create diagnostics, tree view, etc.

public getDetectionById(detectionId: string): ScanResult | undefined {
const detections = getWorkspaceState(getDetectionsKey()) as LocalStorage;
return detections[detectionId] as ScanResult | undefined;
return _slowDeepClone(detections[detectionId]) as ScanResult | undefined;
}

public getDetections(scanType: ScanType): AnyDetection[] {
const scanTypeKey = getScanTypeKey(scanType);
return getWorkspaceState(scanTypeKey) as AnyDetection[] || [];
const detections = getWorkspaceState(scanTypeKey) as AnyDetection[] || [];
return _slowDeepClone(detections);
}

public clearDetections(scanType: ScanType): void {
updateWorkspaceState(getScanTypeKey(scanType), []);
}

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

public saveDetection(scanType: ScanType, detection: AnyDetection): void {
const scanTypeKey = getScanTypeKey(scanType);
public setDetections(scanType: ScanType, detections: AnyDetection[]): void {
// TODO(MarshalX): smart merge with existing detections will be cool someday
this.clearDetections(scanType);
this.saveDetections(scanType, detections);
}

const scanTypeDetections = getWorkspaceState(scanTypeKey) as AnyDetection[] || [];
public saveDetection(scanType: ScanType, detection: AnyDetection): void {
const scanTypeDetections = this.getDetections(scanType);
scanTypeDetections.push(detection);
updateWorkspaceState(scanTypeKey, scanTypeDetections);
updateWorkspaceState(getScanTypeKey(scanType), scanTypeDetections);

const detectionsKey = getDetectionsKey();
const detections = getWorkspaceState(detectionsKey) as LocalStorage;
Expand All @@ -71,7 +89,7 @@ class ScanResultsService {
updateWorkspaceState(detectionsKey, {});

for (const scanType of Object.values(ScanType)) {
updateWorkspaceState(getScanTypeKey(scanType), []);
this.clearDetections(scanType);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function auth(params: CommandParams) {
handleAuthStatus(exitCode, result, stderr);
} catch (error) {
captureException(error);
extensionOutput.error('Error while creating scan: ' + error);
extensionOutput.error('Error while authing: ' + error);
onAuthFailure();
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/services/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {onAuthFailure} from '../utils/auth/auth_common';
import {getHasDetectionState, VscodeStates} from '../utils/states';
import {ProgressBar} from '../cli-wrapper/types';
import {DIAGNOSTIC_CODE_SEPARATOR, ScanType} from '../constants';
import {AnyDetection} from '../types/detection';
import {scanResultsService} from './ScanResultsService';

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

export const updateDetectionState = (scanType: ScanType, detections: AnyDetection[]) => {
export const updateDetectionState = (scanType: ScanType) => {
const detections = scanResultsService.getDetections(scanType);
const hasDetections = detections.length > 0;

updateHasDetectionState(scanType, hasDetections);
Expand Down
44 changes: 44 additions & 0 deletions src/services/diagnostics/IacDiagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as path from 'path';
import * as vscode from 'vscode';
import {IacDetection} from '../../types/detection';
import {extensionId} from '../../utils/texts';
import {DiagnosticCode} from '../common';
import {ScanType} from '../../constants';
import {calculateUniqueDetectionId} from '../ScanResultsService';
import {FileDiagnostics} from './types';

export const createDiagnostics = async (
detections: IacDetection[],
): Promise<FileDiagnostics> => {
const result: FileDiagnostics = {};

for (const detection of detections) {
const {detection_details} = detection;

const documentPath = detection_details.file_name;
const documentUri = vscode.Uri.file(documentPath);
const document = await vscode.workspace.openTextDocument(documentUri);

let message = `Severity: ${detection.severity}\n`;
message += `Rule: ${detection.message}\n`;

message += `IaC Provider: ${detection_details.infra_provider}\n`;

const fileName = path.basename(detection_details.file_name);
message += `In file: ${fileName}\n`;

const diagnostic = new vscode.Diagnostic(
document.lineAt(detection_details.line_in_file - 1).range,
message,
vscode.DiagnosticSeverity.Error
);

diagnostic.source = extensionId;
diagnostic.code = new DiagnosticCode(ScanType.Iac, calculateUniqueDetectionId(detection)).toString();

result[documentPath] = result[documentPath] || [];
result[documentPath].push(diagnostic);
}

return result;
};
39 changes: 39 additions & 0 deletions src/services/diagnostics/SastDiagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as vscode from 'vscode';
import {SastDetection} from '../../types/detection';
import {extensionId} from '../../utils/texts';
import {DiagnosticCode} from '../common';
import {ScanType} from '../../constants';
import {calculateUniqueDetectionId} from '../ScanResultsService';
import {FileDiagnostics} from './types';

export const createDiagnostics = async (
detections: SastDetection[],
): Promise<FileDiagnostics> => {
const result: FileDiagnostics = {};

for (const detection of detections) {
const {detection_details} = detection;

const documentPath = detection_details.file_path;
const documentUri = vscode.Uri.file(documentPath);
const document = await vscode.workspace.openTextDocument(documentUri);

let message = `Severity: ${detection.severity}\n`;
message += `Rule: ${detection.detection_details.policy_display_name}\n`;
message += `In file: ${detection.detection_details.file_name}\n`;

const diagnostic = new vscode.Diagnostic(
document.lineAt(detection_details.line_in_file - 1).range,
message,
vscode.DiagnosticSeverity.Error
);

diagnostic.source = extensionId;
diagnostic.code = new DiagnosticCode(ScanType.Sast, calculateUniqueDetectionId(detection)).toString();

result[documentPath] = result[documentPath] || [];
result[documentPath].push(diagnostic);
}

return result;
};
48 changes: 48 additions & 0 deletions src/services/diagnostics/ScaDiagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as path from 'path';
import * as vscode from 'vscode';
import {ScaDetection} from '../../types/detection';
import {getPackageFileForLockFile, isSupportedLockFile, ScanType} from '../../constants';
import {extensionId} from '../../utils/texts';
import {DiagnosticCode} from '../common';
import {calculateUniqueDetectionId} from '../ScanResultsService';
import {FileDiagnostics} from './types';

export const createDiagnostics = async (
detections: ScaDetection[]
): Promise<FileDiagnostics> => {
const result: FileDiagnostics = {};

for (const detection of detections) {
const {detection_details} = detection;
const file_name = detection_details.file_name;
const uri = vscode.Uri.file(file_name);
const document = await vscode.workspace.openTextDocument(uri);

let message = `Severity: ${detection.severity}\n`;
message += `${detection.message}\n`;
if (detection_details.alert?.first_patched_version) {
message += `First patched version: ${detection_details.alert?.first_patched_version}\n`;
}

if (isSupportedLockFile(file_name)) {
const packageFileName = getPackageFileForLockFile(path.basename(file_name));
message += `\n\nAvoid manual packages upgrades in lock files.
Update the ${packageFileName} file and re-generate the lock file.`;
}

const diagnostic = new vscode.Diagnostic(
// BE of SCA counts lines from 1, while VSCode counts from 0
document.lineAt(detection_details.line_in_file - 1).range,
message,
vscode.DiagnosticSeverity.Error
);

diagnostic.source = extensionId;
diagnostic.code = new DiagnosticCode(ScanType.Sca, calculateUniqueDetectionId(detection)).toString();

result[file_name] = result[file_name] || [];
result[file_name].push(diagnostic);
}

return result;
};
Loading

0 comments on commit e00a7c4

Please sign in to comment.