From 658037d093493017c46c73c18224b7c5157d734a Mon Sep 17 00:00:00 2001 From: Dmytro Demydenko Date: Tue, 9 Jan 2024 15:55:57 +0200 Subject: [PATCH] [BCE-26147] Add SAST findings to JetBrains --- .../kotlin/com/bridgecrew/CheckovResult.kt | 22 +++- .../results/WeaknessCheckovResult.kt | 7 +- .../services/CheckovResultsListUtils.kt | 2 +- .../services/ResultsCacheService.kt | 2 +- .../ui/CheckovToolWindowManagerPanel.kt | 2 +- .../ui/rightPanel/CheckovErrorRightPanel.kt | 2 +- .../WeaknessDictionaryPanel.kt | 120 +++++++++++++++++- .../extraInfoPanel/WeaknessExtraInfoPanel.kt | 5 +- .../fixtures/CheckovResultFixture.kt | 9 +- .../fixtures/WeaknessCheckovResultFixture.kt | 102 +++++++++++++++ .../services/ResultsCacheServiceTest.kt | 5 +- .../WeaknessDictionaryPanelTest.kt | 59 +++++++++ 12 files changed, 321 insertions(+), 16 deletions(-) rename src/test/kotlin/com/bridgecrew/{services => }/fixtures/CheckovResultFixture.kt (90%) create mode 100644 src/test/kotlin/com/bridgecrew/fixtures/WeaknessCheckovResultFixture.kt create mode 100644 src/test/kotlin/com/bridgecrew/ui/rightPanel/dictionaryDetails/WeaknessDictionaryPanelTest.kt diff --git a/src/main/kotlin/com/bridgecrew/CheckovResult.kt b/src/main/kotlin/com/bridgecrew/CheckovResult.kt index 94fd8e2..af22b32 100644 --- a/src/main/kotlin/com/bridgecrew/CheckovResult.kt +++ b/src/main/kotlin/com/bridgecrew/CheckovResult.kt @@ -44,5 +44,25 @@ data class CheckovResult( val guideline: String = "\"No Guide\")", val code_block: List>, var check_type: String, - val fixed_definition: String = "" + val fixed_definition: String = "", + val cwe: ArrayList = ArrayList(), + val owasp: String = "", + val metadata: Metadata? = null +) + +data class Metadata( + val code_locations: List?, + val taint_mode: List? +) + +data class DataFlow( + val path: String, + val start: CodePosition, + val end: CodePosition, + val code_block: String +) + +data class CodePosition( + val row: Int, + val column: Int ) \ No newline at end of file diff --git a/src/main/kotlin/com/bridgecrew/results/WeaknessCheckovResult.kt b/src/main/kotlin/com/bridgecrew/results/WeaknessCheckovResult.kt index 337b91a..c64c49c 100644 --- a/src/main/kotlin/com/bridgecrew/results/WeaknessCheckovResult.kt +++ b/src/main/kotlin/com/bridgecrew/results/WeaknessCheckovResult.kt @@ -1,5 +1,7 @@ package com.bridgecrew.results +import com.bridgecrew.Metadata + class WeaknessCheckovResult( checkType: CheckType, filePath: String, @@ -13,7 +15,10 @@ class WeaknessCheckovResult( fileLineRange: List, fixDefinition: String?, codeBlock: List>, - val checkName: String) : + val checkName: String, + val cwe: List, + val owasp: String, + val metadata: Metadata?) : BaseCheckovResult( category = Category.WEAKNESSES, checkType, diff --git a/src/main/kotlin/com/bridgecrew/services/CheckovResultsListUtils.kt b/src/main/kotlin/com/bridgecrew/services/CheckovResultsListUtils.kt index f1f7dcd..4fa0c84 100644 --- a/src/main/kotlin/com/bridgecrew/services/CheckovResultsListUtils.kt +++ b/src/main/kotlin/com/bridgecrew/services/CheckovResultsListUtils.kt @@ -152,7 +152,7 @@ class CheckovResultsListUtils { checkovResult.checkType, checkovResult.filePath, checkovResult.resource, checkovResult.name, checkovResult.id, checkovResult.severity, checkovResult.description, checkovResult.guideline, checkovResult.absoluteFilePath, fileLineRange, checkovResult.fixDefinition, - codeBlock, (checkovResult as SecretsCheckovResult).checkName + codeBlock, (checkovResult as WeaknessCheckovResult).checkName, checkovResult.cwe, checkovResult.owasp, checkovResult.metadata ) } } diff --git a/src/main/kotlin/com/bridgecrew/services/ResultsCacheService.kt b/src/main/kotlin/com/bridgecrew/services/ResultsCacheService.kt index da86fb6..f87b9e6 100644 --- a/src/main/kotlin/com/bridgecrew/services/ResultsCacheService.kt +++ b/src/main/kotlin/com/bridgecrew/services/ResultsCacheService.kt @@ -155,7 +155,7 @@ class ResultsCacheService(val project: Project) { val weaknessCheckovResult = WeaknessCheckovResult(checkType, filePath, resource, name, result.check_id, severity, description, result.guideline, fileAbsPath, result.file_line_range, result.fixed_definition, - result.code_block, result.check_name) + result.code_block, result.check_name, result.cwe, result.owasp, result.metadata) checkovResults.add(weaknessCheckovResult) continue } diff --git a/src/main/kotlin/com/bridgecrew/ui/CheckovToolWindowManagerPanel.kt b/src/main/kotlin/com/bridgecrew/ui/CheckovToolWindowManagerPanel.kt index 6bece7c..c4d0cbc 100644 --- a/src/main/kotlin/com/bridgecrew/ui/CheckovToolWindowManagerPanel.kt +++ b/src/main/kotlin/com/bridgecrew/ui/CheckovToolWindowManagerPanel.kt @@ -351,7 +351,7 @@ class CheckovToolWindowManagerPanel(val project: Project) : SimpleToolWindowPane private fun createIconForLineErrors(firstRow: Int, results: List, markup: MarkupModel, document: Document) { val rowInFile = if(firstRow > 0) firstRow - 1 else firstRow val rangeHighlighter: RangeHighlighter = markup.addLineHighlighter(rowInFile, HighlighterLayer.ERROR, null) - val bubbleLocation = if(rowInFile >= document.lineCount) document.getLineStartOffset(rowInFile) else document.getLineStartOffset(rowInFile + 1) + val bubbleLocation = if(rowInFile >= document.lineCount - 1) document.getLineStartOffset(rowInFile) else document.getLineStartOffset(rowInFile + 1) val gutterIconRenderer = CheckovGutterErrorIcon(project, results, bubbleLocation, markup, rowInFile) rangeHighlighter.gutterIconRenderer = gutterIconRenderer rangeHighlighter.putUserData(key, true) diff --git a/src/main/kotlin/com/bridgecrew/ui/rightPanel/CheckovErrorRightPanel.kt b/src/main/kotlin/com/bridgecrew/ui/rightPanel/CheckovErrorRightPanel.kt index 1b8850e..c348458 100644 --- a/src/main/kotlin/com/bridgecrew/ui/rightPanel/CheckovErrorRightPanel.kt +++ b/src/main/kotlin/com/bridgecrew/ui/rightPanel/CheckovErrorRightPanel.kt @@ -37,7 +37,7 @@ class CheckovErrorRightPanel(val project: Project, var result: BaseCheckovResult Category.VULNERABILITIES -> VulnerabilitiesExtraInfoPanel(result as VulnerabilityCheckovResult) Category.SECRETS -> SecretsExtraInfoPanel(result as SecretsCheckovResult) Category.LICENSES -> LicenseExtraInfoPanel(result as LicenseCheckovResult) - Category.WEAKNESSES -> WeaknessExtraInfoPanel(result as WeaknessCheckovResult) + Category.WEAKNESSES -> WeaknessExtraInfoPanel(result as WeaknessCheckovResult, project) } return ScrollPaneFactory.createScrollPane( extraInfoPanel, diff --git a/src/main/kotlin/com/bridgecrew/ui/rightPanel/dictionaryDetails/WeaknessDictionaryPanel.kt b/src/main/kotlin/com/bridgecrew/ui/rightPanel/dictionaryDetails/WeaknessDictionaryPanel.kt index 12b4138..4bbeb19 100644 --- a/src/main/kotlin/com/bridgecrew/ui/rightPanel/dictionaryDetails/WeaknessDictionaryPanel.kt +++ b/src/main/kotlin/com/bridgecrew/ui/rightPanel/dictionaryDetails/WeaknessDictionaryPanel.kt @@ -1,15 +1,45 @@ package com.bridgecrew.ui.rightPanel.dictionaryDetails +import com.bridgecrew.DataFlow import com.bridgecrew.results.WeaknessCheckovResult +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.LogicalPosition +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.ui.JBColor +import com.intellij.util.ui.JBUI +import java.awt.Cursor +import java.awt.Dimension +import java.awt.Font +import java.awt.GridBagConstraints +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.io.File +import javax.swing.JLabel -class WeaknessDictionaryPanel(result: WeaknessCheckovResult): DictionaryExtraInfoPanel() { +data class DataFlowDictionary( + val fileName: String, + val path: String, + val codeBlock: String, + val column: Int, + val row: Int, +) + +class WeaknessDictionaryPanel(private val result: WeaknessCheckovResult, private val project: Project) : DictionaryExtraInfoPanel() { override var fieldsMap: MutableMap = mutableMapOf( "Description" to result.description, - "Code" to extractCode(result) + "Code" to extractCode(result), + "CWE(s)" to result.cwe.joinToString(", "), + "OWASP Top 10" to result.owasp, + "Data flow" to extractDataFlow(result) ) + init { addCustomPolicyGuidelinesIfNeeded(result) createDictionaryLayout() + createDataFlowLayout() } private fun extractCode(result: WeaknessCheckovResult): Any { @@ -19,4 +49,90 @@ class WeaknessDictionaryPanel(result: WeaknessCheckovResult): DictionaryExtraInf "" } } + + private fun extractDataFlow(result: WeaknessCheckovResult): String? { + val dataFlow = result.metadata?.code_locations ?: result.metadata?.taint_mode + if (dataFlow !== null) { + return this.calculateDataFlow(dataFlow); + } + return null + } + + private fun calculateDataFlow(dataFlowList: List): String { + val filesCount = dataFlowList.map { it.path }.distinct().count() + val stepsCount = dataFlowList.count() + return "$stepsCount steps in $filesCount file(s)" + } + + + private fun getDataFlowDictionary(result: WeaknessCheckovResult): Array? { + val dataFlow = result.metadata?.code_locations ?: result.metadata?.taint_mode + + if (dataFlow !== null) { + return dataFlow.map { + val line = it.start.row.toString() + if (it.start.row != it.end.row) "-${it.end.row}" else "" + DataFlowDictionary("${File(it.path).name}: $line", it.path, it.code_block, it.start.column, it.start.row) + }.toTypedArray() + } + return null + } + + private fun openFileAtLine(project: Project, absPath: String, line: Int, column: Int) { + val virtualFile = LocalFileSystem.getInstance().findFileByPath(absPath) + if (virtualFile != null) { + val editor: Editor? = FileEditorManager.getInstance(project).openTextEditor(OpenFileDescriptor(project, virtualFile, line, column), true) + editor?.caretModel?.moveToLogicalPosition(LogicalPosition(line, column)) + } + } + + + private fun createDataFlowLayout() { + val dataArray = this.getDataFlowDictionary(this.result) ?: return + val dictionaryFont = Font("SF Pro Text", Font.BOLD, 12) + + val maxKeyWidth = dataArray.maxOfOrNull { + getFontMetrics(dictionaryFont).stringWidth(it.fileName) + } ?: 0 + + val keyConstraints = GridBagConstraints().apply { + weightx = 0.0 + fill = GridBagConstraints.VERTICAL + anchor = GridBagConstraints.LINE_START + insets = JBUI.insets(1, 0, 1, 15) + } + + val valueConstraints = GridBagConstraints().apply { + weightx = 1.0 + fill = GridBagConstraints.HORIZONTAL + gridwidth = GridBagConstraints.REMAINDER + insets = JBUI.insetsBottom(10) + } + + val boldFont = Font(dictionaryFont.name, Font.BOLD, dictionaryFont.size) + + for (item in dataArray) { + val valueAsString = if (item.codeBlock.isEmpty()) "---" else item.codeBlock.trim() + // Create a clickable JLabel for the key + val keyLabel = JLabel("${item.fileName}") + keyLabel.font = boldFont + keyLabel.preferredSize = Dimension(maxKeyWidth + 50, keyLabel.preferredSize.height) + keyLabel.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + keyLabel.toolTipText = item.fileName + keyLabel.setOpaque(true); // Make the JLabel opaque to show the background color + keyLabel.setBackground(JBColor.PanelBackground); + // Add mouse listener to the key label + keyLabel.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + openFileAtLine(project, item.path, item.row, item.column) + } + }) + + add(keyLabel, keyConstraints) + val valueLabel = JLabel(valueAsString) + valueLabel.font = dictionaryFont.deriveFont(Font.PLAIN) + valueLabel.toolTipText = valueAsString + add(valueLabel, valueConstraints) + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/com/bridgecrew/ui/rightPanel/extraInfoPanel/WeaknessExtraInfoPanel.kt b/src/main/kotlin/com/bridgecrew/ui/rightPanel/extraInfoPanel/WeaknessExtraInfoPanel.kt index 4821a73..b591e94 100644 --- a/src/main/kotlin/com/bridgecrew/ui/rightPanel/extraInfoPanel/WeaknessExtraInfoPanel.kt +++ b/src/main/kotlin/com/bridgecrew/ui/rightPanel/extraInfoPanel/WeaknessExtraInfoPanel.kt @@ -2,12 +2,13 @@ package com.bridgecrew.ui.rightPanel.extraInfoPanel import com.bridgecrew.results.WeaknessCheckovResult import com.bridgecrew.ui.rightPanel.dictionaryDetails.WeaknessDictionaryPanel +import com.intellij.openapi.project.Project -class WeaknessExtraInfoPanel(result: WeaknessCheckovResult) : CheckovExtraInfoPanel(result) { +class WeaknessExtraInfoPanel(result: WeaknessCheckovResult, project: Project) : CheckovExtraInfoPanel(result) { init { initLayout() - add(WeaknessDictionaryPanel(result)) + add(WeaknessDictionaryPanel(result, project)) addCodeDiffPanel() } } \ No newline at end of file diff --git a/src/test/kotlin/com/bridgecrew/services/fixtures/CheckovResultFixture.kt b/src/test/kotlin/com/bridgecrew/fixtures/CheckovResultFixture.kt similarity index 90% rename from src/test/kotlin/com/bridgecrew/services/fixtures/CheckovResultFixture.kt rename to src/test/kotlin/com/bridgecrew/fixtures/CheckovResultFixture.kt index 482436a..905f1ab 100644 --- a/src/test/kotlin/com/bridgecrew/services/fixtures/CheckovResultFixture.kt +++ b/src/test/kotlin/com/bridgecrew/fixtures/CheckovResultFixture.kt @@ -1,4 +1,4 @@ -package com.bridgecrew.services.fixtures +package com.bridgecrew.fixtures import com.bridgecrew.CheckovResult @@ -19,11 +19,11 @@ fun createDefaultResults(): CheckovResult { guideline = "", code_block = listOf(listOf(1.0, "class MyBadImplementation extends java.security.MessageDigest {], [2.0 ], [3.0, }")), check_type = "sast_java", - fixed_definition = "" + fixed_definition = "", ) } -fun createSastCheckovResultResults(): CheckovResult { +fun createSastCheckovResult(): CheckovResult { return CheckovResult( check_id = "CKV3_SAST_13", bc_check_id = "", @@ -40,6 +40,7 @@ fun createSastCheckovResultResults(): CheckovResult { guideline = "", code_block = listOf(listOf(1.0, "class MyBadImplementation extends java.security.MessageDigest {], [2.0 ], [3.0, }")), check_type = "sast_java", + cwe = arrayListOf("CWE-327: Use of a Broken or Risky Cryptographic Algorithm"), fixed_definition = "" ) -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/bridgecrew/fixtures/WeaknessCheckovResultFixture.kt b/src/test/kotlin/com/bridgecrew/fixtures/WeaknessCheckovResultFixture.kt new file mode 100644 index 0000000..af2bc21 --- /dev/null +++ b/src/test/kotlin/com/bridgecrew/fixtures/WeaknessCheckovResultFixture.kt @@ -0,0 +1,102 @@ +package com.bridgecrew.fixtures + +import com.bridgecrew.results.WeaknessCheckovResult +import com.google.gson.Gson + + + +const val metadataCodeCollectionJson = """ + "metadata": { + "code_locations": [{ + "path": "/Users/sast-core/tests_policies/src/python/EncryptionKeySize2.py", + "start": { + "row": 13, + "column": 0 + }, + "end": { + "row": 13, + "column": 66 + }, + "code_block": "cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key" + }, { + "path": "/Users/sast-core/tests_policies/src/python/EncryptionKeySize.py", + "start": { + "row": 14, + "column": 73 + }, + "end": { + "row": 15, + "column": 86 + }, + "code_block": "key_size=size" + }] + }, +""" + +const val metadataTaintModeJson = """ + "metadata": { + "taint_mode": [{ + "path": "/Users/sast-core/tests_policies/src/python/EncryptionKeySize.py", + "start": { + "row": 13, + "column": 0 + }, + "end": { + "row": 13, + "column": 66 + }, + "code_block": "cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key" + }, { + "path": "/Users/sast-core/tests_policies/src/python/EncryptionKeySize.py", + "start": { + "row": 14, + "column": 73 + }, + "end": { + "row": 15, + "column": 86 + }, + "code_block": "key_size=size" + }] + }, +""" + +const val metadataEmptyJson = """ + "metadata": { + }, +""" + +const val metadataAbsent = "" + + + +fun createWeaknessCheckovResult(metadata: String): WeaknessCheckovResult { + val resultJson = """ + { + "checkName": "Unsafe custom MessageDigest is implemented", + "cwe": ["CWE-327: Use of a Broken or Risky Cryptographic Algorithm"], + "owasp": "TBD", + $metadata + "category": "WEAKNESSES", + "checkType": "SAST", + "filePath": "/features/sast/BrokenCryptographicAlgorithm/fail.java", + "resource": "", + "name": "Unsafe custom MessageDigest is implemented (1 - 2)", + "id": "CKV3_SAST_13", + "severity": "MEDIUM", + "guideline": "https://docs.paloaltonetworks.com/prisma/prisma-cloud/prisma-cloud-code-security-policy-reference/sast-policies/java-policies/sast-policy-13", + "absoluteFilePath": "/Users/user/testing-resources/features/sast/BrokenCryptographicAlgorithm/fail.java", + "fileLineRange": [1, 2], + "codeBlock": [ + [1.0, "class MyBadImplementation extends java.security.MessageDigest {\n"], + [2.0, "}\n"] + ], + "codeDiffFirstLine": 1 + } + """.trimIndent() + + val gson = Gson() + val userObject: WeaknessCheckovResult = gson.fromJson(resultJson, WeaknessCheckovResult::class.java) + + return userObject +} diff --git a/src/test/kotlin/com/bridgecrew/services/ResultsCacheServiceTest.kt b/src/test/kotlin/com/bridgecrew/services/ResultsCacheServiceTest.kt index 1204a09..83d4151 100644 --- a/src/test/kotlin/com/bridgecrew/services/ResultsCacheServiceTest.kt +++ b/src/test/kotlin/com/bridgecrew/services/ResultsCacheServiceTest.kt @@ -1,8 +1,9 @@ package com.bridgecrew.services +import com.bridgecrew.fixtures.createSastCheckovResult import com.bridgecrew.results.Category import com.bridgecrew.results.CheckType -import com.bridgecrew.services.fixtures.* +import com.bridgecrew.fixtures.* import com.intellij.mock.MockProject import com.intellij.openapi.util.Disposer import org.jetbrains.annotations.SystemIndependent @@ -26,7 +27,7 @@ class ResultsCacheServiceTest { fun `setCheckovResultsFromResultsList should set WeaknessCheckovResult`() { val resultsCacheService = ResultsCacheService(project) - val checkovResult = createSastCheckovResultResults() + val checkovResult = createSastCheckovResult() resultsCacheService.setCheckovResultsFromResultsList(listOf(checkovResult)); assertEquals(resultsCacheService.checkovResults.size, 1) diff --git a/src/test/kotlin/com/bridgecrew/ui/rightPanel/dictionaryDetails/WeaknessDictionaryPanelTest.kt b/src/test/kotlin/com/bridgecrew/ui/rightPanel/dictionaryDetails/WeaknessDictionaryPanelTest.kt new file mode 100644 index 0000000..701b4b1 --- /dev/null +++ b/src/test/kotlin/com/bridgecrew/ui/rightPanel/dictionaryDetails/WeaknessDictionaryPanelTest.kt @@ -0,0 +1,59 @@ +package com.bridgecrew.ui.rightPanel.dictionaryDetails + +import com.bridgecrew.fixtures.* +import com.bridgecrew.fixtures.metadataTaintModeJson +import com.intellij.mock.MockProject +import com.intellij.openapi.util.Disposer +import org.jetbrains.annotations.SystemIndependent +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class WeaknessDictionaryPanelTest { + private val rootDisposable = Disposer.newDisposable() + private val project: MockProject = createProject("/tmp/foo") + + private fun createProject(basePath: String): MockProject { + return object : MockProject(null, rootDisposable) { + override fun getBasePath(): @SystemIndependent String? { + return basePath + } + } + } + + @Nested + inner class ExtractDataFlow{ + @Test + fun `should return empty string WHEN metadata is not provided`() { + val checkovResult = createWeaknessCheckovResult(metadataAbsent) + val panel = WeaknessDictionaryPanel(checkovResult, project) + + assertEquals(panel.fieldsMap["Data flow"], null) + } + + @Test + fun `should return empty string WHEN metadata is empty object`() { + val checkovResult = createWeaknessCheckovResult(metadataEmptyJson) + val panel = WeaknessDictionaryPanel(checkovResult, project) + + assertEquals(panel.fieldsMap["Data flow"], null) + } + + @Test + fun `should return Data flow WHEN taint_mode is provided `() { + val checkovResult = createWeaknessCheckovResult(metadataTaintModeJson) + val panel = WeaknessDictionaryPanel(checkovResult, project) + + assertEquals(panel.fieldsMap["Data flow"], "2 steps in 1 file(s)") + } + + @Test + fun `should return Data flow WHEN code_locations is provided `() { + val checkovResult = createWeaknessCheckovResult(metadataCodeCollectionJson) + val panel = WeaknessDictionaryPanel(checkovResult, project) + + assertEquals(panel.fieldsMap["Data flow"], "2 steps in 2 file(s)") + } + } + +}