From e00a7c4a75fabf4cc25e555ddd887c9239e25031 Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Fri, 2 Aug 2024 11:42:16 +0200 Subject: [PATCH] CM-38538 - Rework scan results handling (#95) --- CHANGELOG.md | 6 + package.json | 2 +- src/extension.ts | 11 +- src/providers/tree-view/utils.ts | 35 ++---- src/services/ScanResultsService.ts | 32 +++-- src/services/auth.ts | 2 +- src/services/common.ts | 5 +- src/services/diagnostics/IacDiagnostics.ts | 44 +++++++ src/services/diagnostics/SastDiagnostics.ts | 39 ++++++ src/services/diagnostics/ScaDiagnostics.ts | 48 ++++++++ src/services/diagnostics/SecretDiagnostics.ts | 40 +++++++ src/services/diagnostics/common.ts | 57 +++++++++ src/services/diagnostics/types.ts | 3 + src/services/scanners/IacScanner.ts | 112 +++--------------- src/services/scanners/SastScanner.ts | 93 +++------------ src/services/scanners/ScaScanner.ts | 109 ++--------------- src/services/scanners/SecretScanner.ts | 95 ++------------- src/services/scanners/common.ts | 36 ++++++ 18 files changed, 377 insertions(+), 392 deletions(-) create mode 100644 src/services/diagnostics/IacDiagnostics.ts create mode 100644 src/services/diagnostics/SastDiagnostics.ts create mode 100644 src/services/diagnostics/ScaDiagnostics.ts create mode 100644 src/services/diagnostics/SecretDiagnostics.ts create mode 100644 src/services/diagnostics/common.ts create mode 100644 src/services/diagnostics/types.ts create mode 100644 src/services/scanners/common.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a3cfe4..97c84dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## [v1.9.3] + +- Rework scan results handling + ## [v1.9.2] - Fix CodeLens updating @@ -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 diff --git a/package.json b/package.json index 1f32b2b..238d805 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/extension.ts b/src/extension.ts index e05c96f..ab37d7c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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(); @@ -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); @@ -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( diff --git a/src/providers/tree-view/utils.ts b/src/providers/tree-view/utils.ts index 48aab6b..6be03b3 100644 --- a/src/providers/tree-view/utils.ts +++ b/src/providers/tree-view/utils.ts @@ -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; @@ -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; @@ -108,10 +99,10 @@ const _getSastValueItem = (detection: SastDetection): ValueItem => { return {fullFilePath: file_path, data: valueItem}; }; -function mapDetectionsByFileName( +const mapDetectionsByFileName = ( detections: AnyDetection[], scanType: ScanType, -): Map { +): Map => { const resultMap: Map = new Map(); detections.forEach((detection) => { @@ -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; @@ -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 = {}; diff --git a/src/services/ScanResultsService.ts b/src/services/ScanResultsService.ts index 7fd53e3..7ed0d31 100644 --- a/src/services/ScanResultsService.ts +++ b/src/services/ScanResultsService.ts @@ -31,15 +31,29 @@ interface ScanResult { type LocalStorage = Record; +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 { @@ -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; @@ -71,7 +89,7 @@ class ScanResultsService { updateWorkspaceState(detectionsKey, {}); for (const scanType of Object.values(ScanType)) { - updateWorkspaceState(getScanTypeKey(scanType), []); + this.clearDetections(scanType); } } } diff --git a/src/services/auth.ts b/src/services/auth.ts index 969f1a5..a4ea11a 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -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(); } } diff --git a/src/services/common.ts b/src/services/common.ts index 951d6f6..877a96d 100644 --- a/src/services/common.ts +++ b/src/services/common.ts @@ -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'; @@ -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); diff --git a/src/services/diagnostics/IacDiagnostics.ts b/src/services/diagnostics/IacDiagnostics.ts new file mode 100644 index 0000000..09d2812 --- /dev/null +++ b/src/services/diagnostics/IacDiagnostics.ts @@ -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 => { + 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; +}; diff --git a/src/services/diagnostics/SastDiagnostics.ts b/src/services/diagnostics/SastDiagnostics.ts new file mode 100644 index 0000000..d2ea0eb --- /dev/null +++ b/src/services/diagnostics/SastDiagnostics.ts @@ -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 => { + 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; +}; diff --git a/src/services/diagnostics/ScaDiagnostics.ts b/src/services/diagnostics/ScaDiagnostics.ts new file mode 100644 index 0000000..02fd294 --- /dev/null +++ b/src/services/diagnostics/ScaDiagnostics.ts @@ -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 => { + 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; +}; diff --git a/src/services/diagnostics/SecretDiagnostics.ts b/src/services/diagnostics/SecretDiagnostics.ts new file mode 100644 index 0000000..5b8928e --- /dev/null +++ b/src/services/diagnostics/SecretDiagnostics.ts @@ -0,0 +1,40 @@ +import * as vscode from 'vscode'; +import {SecretDetection} from '../../types/detection'; +import {extensionId} from '../../utils/texts'; +import {DiagnosticCode} from '../common'; +import {ScanType} from '../../constants'; +import {calculateUniqueDetectionId} from '../ScanResultsService'; +import {getSecretDetectionIdeData} from '../scanners/SecretScanner'; +import {FileDiagnostics} from './types'; + +export const createDiagnostics = async ( + detections: SecretDetection[], +): Promise => { + const result: FileDiagnostics = {}; + + for (const detection of detections) { + const ideData = await getSecretDetectionIdeData(detection); + + let message = `Severity: ${detection.severity}\n`; + message += `${detection.type}: ${detection.message.replace( + 'within \'\' repository', + '' + )}\n`; + message += `In file: ${detection.detection_details.file_name}\n`; + message += `Secret SHA: ${detection.detection_details.sha512}`; + + const diagnostic = new vscode.Diagnostic( + ideData.range, + message, + vscode.DiagnosticSeverity.Error + ); + + diagnostic.source = extensionId; + diagnostic.code = new DiagnosticCode(ScanType.Secrets, calculateUniqueDetectionId(detection)).toString(); + + result[ideData.documentPath] = result[ideData.documentPath] || []; + result[ideData.documentPath].push(diagnostic); + } + + return result; +}; diff --git a/src/services/diagnostics/common.ts b/src/services/diagnostics/common.ts new file mode 100644 index 0000000..b63b05a --- /dev/null +++ b/src/services/diagnostics/common.ts @@ -0,0 +1,57 @@ +import * as vscode from 'vscode'; +import {AnyDetection, IacDetection, SastDetection, ScaDetection, SecretDetection} from '../../types/detection'; +import {createDiagnostics as createDiagnosticsSecret} from './SecretDiagnostics'; +import {createDiagnostics as createDiagnosticsSca} from './ScaDiagnostics'; +import {createDiagnostics as createDiagnosticsIac} from './IacDiagnostics'; +import {createDiagnostics as createDiagnosticsSast} from './SastDiagnostics'; +import {ScanType} from '../../constants'; +import {scanResultsService} from '../ScanResultsService'; +import {FileDiagnostics} from './types'; +import {validateTextRangeInOpenDoc} from '../../utils/range'; + +const createDiagnostics = async ( + scanType: ScanType, detections: AnyDetection[] +): Promise => { + switch (scanType) { + case ScanType.Secrets: + return await createDiagnosticsSecret(detections as SecretDetection[]); + case ScanType.Sca: + return await createDiagnosticsSca(detections as ScaDetection[]); + case ScanType.Iac: + return await createDiagnosticsIac(detections as IacDetection[]); + case ScanType.Sast: + return await createDiagnosticsSast(detections as SastDetection[]); + default: + throw new Error('Unsupported scan type'); + } +}; + +const setDiagnostics = ( + diagnostics: FileDiagnostics, diagnosticCollection: vscode.DiagnosticCollection +) => { + for (const [filePath, fileDiagnostics] of Object.entries(diagnostics)) { + const uri = vscode.Uri.file(filePath); + + const validFileDiagnostics = fileDiagnostics.filter((diagnostic) => { + return validateTextRangeInOpenDoc(uri, diagnostic.range); + }); + + diagnosticCollection.set(uri, validFileDiagnostics); + } +}; + +const updateDiagnosticCollection = async ( + scanType: ScanType, detections: AnyDetection[], diagnosticCollection: vscode.DiagnosticCollection +) => { + const diagnostics = await createDiagnostics(scanType, detections); + setDiagnostics(diagnostics, diagnosticCollection); +}; + +export const refreshDiagnosticCollectionData = async (diagnosticCollection: vscode.DiagnosticCollection) => { + diagnosticCollection.clear(); + + for (const scanType of Object.values(ScanType)) { + const detections = scanResultsService.getDetections(scanType); + await updateDiagnosticCollection(scanType, detections, diagnosticCollection); + } +}; diff --git a/src/services/diagnostics/types.ts b/src/services/diagnostics/types.ts new file mode 100644 index 0000000..3589c34 --- /dev/null +++ b/src/services/diagnostics/types.ts @@ -0,0 +1,3 @@ +import * as vscode from 'vscode'; + +export type FileDiagnostics = Record; diff --git a/src/services/scanners/IacScanner.ts b/src/services/scanners/IacScanner.ts index 0cee2e0..ab75254 100644 --- a/src/services/scanners/IacScanner.ts +++ b/src/services/scanners/IacScanner.ts @@ -1,26 +1,21 @@ import * as vscode from 'vscode'; -import * as path from 'path'; import {extensionOutput} from '../../logging/extension-output'; import {cliWrapper} from '../../cli-wrapper/cli-wrapper'; import statusBar from '../../utils/status-bar'; -import {extensionId, StatusBarTexts, TrayNotificationTexts} from '../../utils/texts'; +import {StatusBarTexts, TrayNotificationTexts} from '../../utils/texts'; import { - DiagnosticCode, finalizeScanState, - updateDetectionState, validateCliCommonErrors, validateCliCommonScanErrors, } from '../common'; import {getWorkspaceState, updateWorkspaceState} from '../../utils/context'; import {IacDetection} from '../../types/detection'; import {IConfig, ProgressBar, RunCliResult} from '../../cli-wrapper/types'; -import TrayNotifications from '../../utils/TrayNotifications'; -import {refreshTreeViewData} from '../../providers/tree-view/utils'; import {TreeView} from '../../providers/tree-view/types'; import {ScanType} from '../../constants'; import {VscodeStates} from '../../utils/states'; -import {calculateUniqueDetectionId, scanResultsService} from '../ScanResultsService'; import {captureException} from '../../sentry'; +import {handleScanResult} from './common'; interface IacScanParams { pathToScan: string; @@ -30,13 +25,12 @@ interface IacScanParams { onDemand?: boolean; } -export const iacScan = ( - params: IacScanParams, - treeView?: TreeView, -) => { +type IacScanResult = { detections?: IacDetection[] }; + +export const iacScan = (params: IacScanParams, treeView: TreeView) => { // we are showing progress bar only for on-demand scans if (!params.onDemand) { - _iacScan(params, undefined, undefined, treeView); + _iacScan(params, treeView, undefined, undefined); return; } @@ -46,7 +40,7 @@ export const iacScan = ( cancellable: true, }, async (progress, token) => { - await _iacScan(params, progress, token, treeView); + await _iacScan(params, treeView, progress, token); }, ); }; @@ -73,34 +67,26 @@ const _initScanState = (params: IacScanParams, progress?: ProgressBar) => { }); }; -const filterUnsupportedIacDetections = (result: { detections?: IacDetection[] }): IacDetection[] => { - const filteredResult: IacDetection[] = []; - +const filterUnsupportedIacDetections = (result: IacScanResult): IacScanResult => { if (!result || !result.detections) { - return filteredResult; + return result; } - for (const detection of result.detections) { - const {detection_details} = detection; - + result.detections = result.detections.filter((detection) => { // TF plans are virtual files what is not exist in the file system // "file_name": "1711298252-/Users/ilyasiamionau/projects/cycode/ilya-siamionau-payloads/tfplan.tf", // skip such detections - if (!detection_details.file_name.startsWith('/')) { - continue; - } - - filteredResult.push(detection); - } + return detection.detection_details.file_name.startsWith('/'); + }); - return filteredResult; + return result; }; export async function _iacScan( params: IacScanParams, + treeView: TreeView, progress?: ProgressBar, cancellationToken?: vscode.CancellationToken, - treeView?: TreeView, ) { try { if (getWorkspaceState(VscodeStates.IacScanInProgress)) { @@ -130,8 +116,8 @@ export async function _iacScan( } validateCliCommonScanErrors(result); - // Show in "problems" tab - await handleScanDetections( + await handleScanResult( + ScanType.Iac, filterUnsupportedIacDetections(result), params.diagnosticCollection, treeView @@ -149,70 +135,6 @@ export async function _iacScan( } vscode.window.showErrorMessage(notificationText); - extensionOutput.error('Error while creating scan: ' + error); + extensionOutput.error('Error while creating IaC scan: ' + error); } } - -const detectionsToDiagnostics = async ( - detections: IacDetection[], -): Promise> => { - const result: Record = {}; - - 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.detection_details.infra_provider}\n`; - - const fileName = path.basename(detection.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; -}; - -const handleScanDetections = async ( - detections: IacDetection[], - diagnosticCollection: vscode.DiagnosticCollection, - treeView?: TreeView -) => { - const hasDetections = detections.length > 0; - updateDetectionState(ScanType.Iac, detections); - - const diagnostics = await detectionsToDiagnostics(detections) || []; - for (const [filePath, fileDiagnostics] of Object.entries(diagnostics)) { - const uri = vscode.Uri.file(filePath); - diagnosticCollection.set(uri, fileDiagnostics); // Show in "problems" tab - } - - if (hasDetections && !getWorkspaceState(VscodeStates.NotificationIsOpen)) { - updateWorkspaceState(VscodeStates.NotificationIsOpen, true); - TrayNotifications.showProblemsDetection(detections.length, ScanType.Iac); - } - - scanResultsService.saveDetections(ScanType.Iac, detections); - - refreshTreeViewData({ - detections, - treeView: treeView, - scanType: ScanType.Iac, - }); -}; diff --git a/src/services/scanners/SastScanner.ts b/src/services/scanners/SastScanner.ts index b21d4dd..ae370ff 100644 --- a/src/services/scanners/SastScanner.ts +++ b/src/services/scanners/SastScanner.ts @@ -2,24 +2,20 @@ import * as vscode from 'vscode'; import {extensionOutput} from '../../logging/extension-output'; import {cliWrapper} from '../../cli-wrapper/cli-wrapper'; import statusBar from '../../utils/status-bar'; -import {extensionId, StatusBarTexts, TrayNotificationTexts} from '../../utils/texts'; +import {StatusBarTexts, TrayNotificationTexts} from '../../utils/texts'; import { - DiagnosticCode, finalizeScanState, - updateDetectionState, validateCliCommonErrors, validateCliCommonScanErrors, } from '../common'; import {getWorkspaceState, updateWorkspaceState} from '../../utils/context'; import {SastDetection} from '../../types/detection'; import {IConfig, ProgressBar, RunCliResult} from '../../cli-wrapper/types'; -import TrayNotifications from '../../utils/TrayNotifications'; -import {refreshTreeViewData} from '../../providers/tree-view/utils'; import {TreeView} from '../../providers/tree-view/types'; import {ScanType} from '../../constants'; import {VscodeStates} from '../../utils/states'; -import {calculateUniqueDetectionId, scanResultsService} from '../ScanResultsService'; import {captureException} from '../../sentry'; +import {handleScanResult} from './common'; interface SastScanParams { pathToScan: string; @@ -29,13 +25,12 @@ interface SastScanParams { onDemand?: boolean; } -export const sastScan = ( - params: SastScanParams, - treeView?: TreeView, -) => { +type SastScanResult = { detections?: SastDetection[] }; + +export const sastScan = (params: SastScanParams, treeView: TreeView) => { // we are showing progress bar only for on-demand scans if (!params.onDemand) { - _sastScan(params, undefined, undefined, treeView); + _sastScan(params, treeView, undefined, undefined); return; } @@ -45,7 +40,7 @@ export const sastScan = ( cancellable: true, }, async (progress, token) => { - await _sastScan(params, progress, token, treeView); + await _sastScan(params, treeView, progress, token); }, ); }; @@ -72,9 +67,9 @@ const _initScanState = (params: SastScanParams, progress?: ProgressBar) => { }); }; -const normalizeSastDetections = (result: { detections?: SastDetection[] }): SastDetection[] => { +const normalizeSastDetections = (result: SastScanResult): SastScanResult => { if (!result || !result.detections) { - return []; + return result; } for (const detection of result.detections) { @@ -85,14 +80,14 @@ const normalizeSastDetections = (result: { detections?: SastDetection[] }): Sast } } - return result.detections; + return result; }; export async function _sastScan( params: SastScanParams, + treeView: TreeView, progress?: ProgressBar, cancellationToken?: vscode.CancellationToken, - treeView?: TreeView, ) { try { if (getWorkspaceState(VscodeStates.SastScanInProgress)) { @@ -122,8 +117,8 @@ export async function _sastScan( } validateCliCommonScanErrors(result); - // Show in "problems" tab - await handleScanDetections( + await handleScanResult( + ScanType.Sast, normalizeSastDetections(result), params.diagnosticCollection, treeView @@ -141,66 +136,6 @@ export async function _sastScan( } vscode.window.showErrorMessage(notificationText); - extensionOutput.error('Error while creating scan: ' + error); + extensionOutput.error('Error while creating SAST scan: ' + error); } } - -const detectionsToDiagnostics = async ( - detections: SastDetection[], -): Promise> => { - const result: Record = {}; - - 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; -}; - -const handleScanDetections = async ( - detections: SastDetection[], - diagnosticCollection: vscode.DiagnosticCollection, - treeView?: TreeView -) => { - const hasDetections = detections.length > 0; - updateDetectionState(ScanType.Sast, detections); - - const diagnostics = await detectionsToDiagnostics(detections) || []; - for (const [filePath, fileDiagnostics] of Object.entries(diagnostics)) { - const uri = vscode.Uri.file(filePath); - diagnosticCollection.set(uri, fileDiagnostics); // Show in "problems" tab - } - - if (hasDetections && !getWorkspaceState(VscodeStates.NotificationIsOpen)) { - updateWorkspaceState(VscodeStates.NotificationIsOpen, true); - TrayNotifications.showProblemsDetection(detections.length, ScanType.Sast); - } - - scanResultsService.saveDetections(ScanType.Sast, detections); - - refreshTreeViewData({ - detections, - treeView: treeView, - scanType: ScanType.Sast, - }); -}; diff --git a/src/services/scanners/ScaScanner.ts b/src/services/scanners/ScaScanner.ts index 5793c22..f767349 100644 --- a/src/services/scanners/ScaScanner.ts +++ b/src/services/scanners/ScaScanner.ts @@ -1,26 +1,16 @@ -import * as path from 'path'; import * as vscode from 'vscode'; import {extensionOutput} from '../../logging/extension-output'; import {cliWrapper} from '../../cli-wrapper/cli-wrapper'; import statusBar from '../../utils/status-bar'; -import {extensionId, StatusBarTexts} from '../../utils/texts'; -import { - DiagnosticCode, - finalizeScanState, - updateDetectionState, - validateCliCommonErrors, - validateCliCommonScanErrors, -} from '../common'; +import {StatusBarTexts} from '../../utils/texts'; +import {finalizeScanState, validateCliCommonErrors, validateCliCommonScanErrors} from '../common'; import {getWorkspaceState, updateWorkspaceState} from '../../utils/context'; -import {ScaDetection} from '../../types/detection'; import {IConfig, ProgressBar, RunCliResult} from '../../cli-wrapper/types'; -import TrayNotifications from '../../utils/TrayNotifications'; import {TreeView} from '../../providers/tree-view/types'; -import {refreshTreeViewData} from '../../providers/tree-view/utils'; -import {getPackageFileForLockFile, isSupportedLockFile, ScanType} from '../../constants'; +import {ScanType} from '../../constants'; import {VscodeStates} from '../../utils/states'; -import {calculateUniqueDetectionId, scanResultsService} from '../ScanResultsService'; import {captureException} from '../../sentry'; +import {handleScanResult} from './common'; interface ScaScanParams { pathToScan: string; @@ -30,16 +20,13 @@ interface ScaScanParams { onDemand?: boolean; } -export function scaScan( - params: ScaScanParams, - treeView: TreeView, -) { +export function scaScan(params: ScaScanParams, treeView: TreeView) { if (getWorkspaceState(VscodeStates.ScaScanInProgress)) { return; } if (!params.onDemand) { - _scaScan(params, undefined, undefined, treeView); + _scaScan(params, treeView, undefined, undefined); return; } @@ -49,7 +36,7 @@ export function scaScan( cancellable: true, }, async (progress, token) => { - await _scaScan(params, progress, token, treeView); + await _scaScan(params, treeView, progress, token); }, ); } @@ -80,9 +67,9 @@ const _getRunnableCliScaScan = (params: ScaScanParams): RunCliResult => { const _scaScan = async ( params: ScaScanParams, + treeView: TreeView, progress?: ProgressBar, cancellationToken?: vscode.CancellationToken, - treeView?: TreeView ) => { try { _initScanState(params, progress); @@ -101,7 +88,7 @@ const _scaScan = async ( } validateCliCommonScanErrors(result); - await handleScanDetections(result, params.diagnosticCollection, treeView); + await handleScanResult(ScanType.Sca, result, params.diagnosticCollection, treeView); finalizeScanState(VscodeStates.ScaScanInProgress, true, progress); } catch (error) { @@ -109,82 +96,6 @@ const _scaScan = async ( finalizeScanState(VscodeStates.ScaScanInProgress, false, progress); - extensionOutput.error('Error while creating scan: ' + error); - } -}; - -const detectionsToDiagnostics = async ( - detections: ScaDetection[] -): Promise> => { - const result: Record = {}; - - 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; -}; - -const handleScanDetections = async ( - result: any, - diagnosticCollection: vscode.DiagnosticCollection, - treeView?: TreeView -) => { - const {detections} = result; - - const hasDetections = detections.length > 0; - if (!hasDetections) { - return; - } - - updateDetectionState(ScanType.Sca, detections); - - const diagnostics = await detectionsToDiagnostics(result.detections); - - // add the diagnostics to the diagnostic collection - for (const [filePath, fileDiagnostics] of Object.entries(diagnostics)) { - const uri = vscode.Uri.file(filePath); - diagnosticCollection.set(uri, fileDiagnostics); // Show in "problems" tab - } - - if (result.detections.length && !getWorkspaceState(VscodeStates.NotificationIsOpen)) { - updateWorkspaceState(VscodeStates.NotificationIsOpen, true); - TrayNotifications.showProblemsDetection(result.detections.length, ScanType.Sca); + extensionOutput.error('Error while creating SCA scan: ' + error); } - - scanResultsService.saveDetections(ScanType.Sca, detections); - - refreshTreeViewData({ - detections, - treeView, - scanType: ScanType.Sca, - }); }; diff --git a/src/services/scanners/SecretScanner.ts b/src/services/scanners/SecretScanner.ts index d77917e..7b3efe8 100644 --- a/src/services/scanners/SecretScanner.ts +++ b/src/services/scanners/SecretScanner.ts @@ -3,24 +3,16 @@ import * as vscode from 'vscode'; import {extensionOutput} from '../../logging/extension-output'; import {cliWrapper} from '../../cli-wrapper/cli-wrapper'; import statusBar from '../../utils/status-bar'; -import {extensionId, StatusBarTexts, TrayNotificationTexts} from '../../utils/texts'; -import { - DiagnosticCode, - finalizeScanState, - updateDetectionState, - validateCliCommonErrors, - validateCliCommonScanErrors, -} from '../common'; +import {StatusBarTexts, TrayNotificationTexts} from '../../utils/texts'; +import {finalizeScanState, validateCliCommonErrors, validateCliCommonScanErrors} from '../common'; import {getWorkspaceState, updateWorkspaceState} from '../../utils/context'; import {SecretDetection} from '../../types/detection'; import {IConfig, ProgressBar, RunCliResult} from '../../cli-wrapper/types'; -import TrayNotifications from '../../utils/TrayNotifications'; -import {refreshTreeViewData} from '../../providers/tree-view/utils'; import {TreeView} from '../../providers/tree-view/types'; import {ScanType} from '../../constants'; import {VscodeStates} from '../../utils/states'; -import {calculateUniqueDetectionId, scanResultsService} from '../ScanResultsService'; import {captureException} from '../../sentry'; +import {handleScanResult} from './common'; interface SecretScanParams { pathToScan: string; @@ -30,13 +22,10 @@ interface SecretScanParams { onDemand?: boolean; } -export const secretScan = ( - params: SecretScanParams, - treeView?: TreeView, -) => { +export const secretScan = (params: SecretScanParams, treeView: TreeView) => { // we are showing progress bar only for on-demand scans if (!params.onDemand) { - _secretScan(params, undefined, undefined, treeView); + _secretScan(params, treeView, undefined, undefined); return; } @@ -46,7 +35,7 @@ export const secretScan = ( cancellable: true, }, async (progress, token) => { - await _secretScan(params, progress, token, treeView); + await _secretScan(params, treeView, progress, token); }, ); }; @@ -75,9 +64,9 @@ const _initScanState = (params: SecretScanParams, progress?: ProgressBar) => { export async function _secretScan( params: SecretScanParams, + treeView: TreeView, progress?: ProgressBar, cancellationToken?: vscode.CancellationToken, - treeView?: TreeView, ) { try { if (getWorkspaceState(VscodeStates.SecretsScanInProgress)) { @@ -107,8 +96,8 @@ export async function _secretScan( } validateCliCommonScanErrors(result); - // Show in "problems" tab - await handleScanDetections( + await handleScanResult( + ScanType.Secrets, result, params.diagnosticCollection, treeView @@ -126,7 +115,7 @@ export async function _secretScan( } vscode.window.showErrorMessage(notificationText); - extensionOutput.error('Error while creating scan: ' + error); + extensionOutput.error('Error while creating Secret scan: ' + error); } } @@ -160,67 +149,3 @@ export const getSecretDetectionIdeData = async (detection: SecretDetection): Pro value, }; }; - -const detectionsToDiagnostics = async ( - detections: SecretDetection[], -): Promise> => { - const result: Record = {}; - - for (const detection of detections) { - const ideData = await getSecretDetectionIdeData(detection); - - let message = `Severity: ${detection.severity}\n`; - message += `${detection.type}: ${detection.message.replace( - 'within \'\' repository', - '' - )}\n`; - message += `In file: ${detection.detection_details.file_name}\n`; - message += `Secret SHA: ${detection.detection_details.sha512}`; - - const diagnostic = new vscode.Diagnostic( - ideData.range, - message, - vscode.DiagnosticSeverity.Error - ); - - diagnostic.source = extensionId; - diagnostic.code = new DiagnosticCode(ScanType.Secrets, calculateUniqueDetectionId(detection)).toString(); - - result[ideData.documentPath] = result[ideData.documentPath] || []; - result[ideData.documentPath].push(diagnostic); - } - - return result; -}; - -const handleScanDetections = async ( - result: { detections?: SecretDetection[] }, - diagnosticCollection: vscode.DiagnosticCollection, - treeView?: TreeView -) => { - const {detections} = result; - if (detections === undefined) { - return; - } - - updateDetectionState(ScanType.Secrets, detections); - - const diagnostics = await detectionsToDiagnostics(detections) || []; - for (const [filePath, fileDiagnostics] of Object.entries(diagnostics)) { - const uri = vscode.Uri.file(filePath); - diagnosticCollection.set(uri, fileDiagnostics); // Show in "problems" tab - } - - if (result.detections?.length && !getWorkspaceState(VscodeStates.NotificationIsOpen)) { - updateWorkspaceState(VscodeStates.NotificationIsOpen, true); - TrayNotifications.showProblemsDetection(result.detections.length, ScanType.Secrets); - } - - scanResultsService.saveDetections(ScanType.Secrets, detections); - - refreshTreeViewData({ - detections, - treeView: treeView, - scanType: ScanType.Secrets, - }); -}; diff --git a/src/services/scanners/common.ts b/src/services/scanners/common.ts new file mode 100644 index 0000000..232c6b4 --- /dev/null +++ b/src/services/scanners/common.ts @@ -0,0 +1,36 @@ +import * as vscode from 'vscode'; +import {AnyDetection} from '../../types/detection'; +import {TreeView} from '../../providers/tree-view/types'; +import {updateDetectionState} from '../common'; +import {ScanType} from '../../constants'; +import {refreshDiagnosticCollectionData} from '../diagnostics/common'; +import {getWorkspaceState, updateWorkspaceState} from '../../utils/context'; +import {VscodeStates} from '../../utils/states'; +import TrayNotifications from '../../utils/TrayNotifications'; +import {scanResultsService} from '../ScanResultsService'; +import {refreshTreeViewData} from '../../providers/tree-view/utils'; + +type ScanResult = { detections?: AnyDetection[] }; + +export const handleScanResult = async ( + scanType: ScanType, + result: ScanResult, + diagnosticCollection: vscode.DiagnosticCollection, + treeView: TreeView +) => { + let {detections} = result; + if (!detections) { + detections = []; + } + + scanResultsService.setDetections(scanType, detections); + + updateDetectionState(scanType); + await refreshDiagnosticCollectionData(diagnosticCollection); + refreshTreeViewData(scanType, treeView); + + if (detections.length && !getWorkspaceState(VscodeStates.NotificationIsOpen)) { + updateWorkspaceState(VscodeStates.NotificationIsOpen, true); + TrayNotifications.showProblemsDetection(detections.length, scanType); + } +};