diff --git a/src/main/kotlin/com/github/adrienpessu/sarifviewer/models/View.kt b/src/main/kotlin/com/github/adrienpessu/sarifviewer/models/View.kt index c957cc1..1f3e805 100644 --- a/src/main/kotlin/com/github/adrienpessu/sarifviewer/models/View.kt +++ b/src/main/kotlin/com/github/adrienpessu/sarifviewer/models/View.kt @@ -9,7 +9,8 @@ data class View( companion object { val RULE = View("rules", "View by rules") val LOCATION = View("location", "View by location") - val views = arrayOf(RULE, LOCATION) + val ALERT_NUMBER = View("alert num", "View by GHAS alert number") + val views = arrayOf(RULE, LOCATION, ALERT_NUMBER) } } diff --git a/src/main/kotlin/com/github/adrienpessu/sarifviewer/services/SarifService.kt b/src/main/kotlin/com/github/adrienpessu/sarifviewer/services/SarifService.kt index 8230d62..586befa 100644 --- a/src/main/kotlin/com/github/adrienpessu/sarifviewer/services/SarifService.kt +++ b/src/main/kotlin/com/github/adrienpessu/sarifviewer/services/SarifService.kt @@ -9,10 +9,12 @@ import com.github.adrienpessu.sarifviewer.models.Leaf import com.github.adrienpessu.sarifviewer.models.Root import com.github.adrienpessu.sarifviewer.models.View import com.github.adrienpessu.sarifviewer.utils.GitHubInstance +import com.google.common.base.Strings import com.intellij.openapi.components.Service import com.intellij.util.alsoIfNull import java.net.HttpURLConnection import java.net.URL +import java.util.* @Service(Service.Level.PROJECT) @@ -28,7 +30,7 @@ class SarifService { return ids.map { id -> val sarifFromGitHub = getSarifFromGitHub(github, repositoryFullName, id) val sarif: SarifSchema210 = objectMapper.readValue(sarifFromGitHub) - sarif.alsoIfNull { SarifSchema210() } + sarif.alsoIfNull { SarifSchema210() } } } @@ -47,7 +49,7 @@ class SarifService { } - fun analyseSarif(sarif: SarifSchema210, view: View): HashMap> { + fun analyseSarif(sarif: SarifSchema210, view: View): MutableMap> { when (view) { View.RULE -> { @@ -69,6 +71,7 @@ class SarifService { } return map } + View.LOCATION -> { val map = HashMap>() try { @@ -88,29 +91,64 @@ class SarifService { } return map } + + View.ALERT_NUMBER -> { + val map = TreeMap>(); + try { + sarif.runs.forEach { run -> + run?.results?.forEach { result -> + val element = leaf(result) + val key = if (Strings.isNullOrEmpty(element.githubAlertNumber)) { + "Missing alert number" + } else { + element.githubAlertNumber + } + if (map.containsKey(key)) { + map[key]?.add(element) + } else { + map[key] = mutableListOf(element) + } + } + } + } catch (e: Exception) { + throw SarifViewerException.INVALID_SARIF + } + return map.toSortedMap(Comparator.comparingInt { k -> + try { + Integer.valueOf(k) + } catch (e: NumberFormatException) { + Integer.MIN_VALUE + } + }) + } + else -> { throw SarifViewerException.INVALID_VIEW } } - - } private fun leaf(result: Result): Leaf { val additionalProperties = result.properties?.additionalProperties ?: mapOf() val element = Leaf( - leafName = result.message.text ?: "", - address = "${result.locations[0].physicalLocation.artifactLocation.uri}:${result.locations[0].physicalLocation.region.startLine}", - steps = result.codeFlows?.get(0)?.threadFlows?.get(0)?.locations?.map { "${it.location.physicalLocation.artifactLocation.uri}:${it.location.physicalLocation.region.startLine}" } - ?: listOf(), - location = result.locations[0].physicalLocation.artifactLocation.uri, - ruleId = result.ruleId, - ruleName = result.rule?.id ?: "", - ruleDescription = result.message.text ?: "", - level = result.level.toString(), - kind = result.kind.toString(), - githubAlertNumber = additionalProperties["github/alertNumber"]?.toString() ?: "", - githubAlertUrl = additionalProperties["github/alertUrl"]?.toString() ?: "" + leafName = result.message.text ?: "", + address = "${result.locations[0].physicalLocation.artifactLocation.uri}:${result.locations[0].physicalLocation.region.startLine}", + steps = result.codeFlows?.get(0)?.threadFlows?.get(0)?.locations?.map { + "${it.location.physicalLocation.artifactLocation.uri}:" + + "${it.location.physicalLocation.region.startLine}:" + + "${it.location.physicalLocation.region.startColumn}:" + + "${it.location.physicalLocation.region.endLine}:" + + "${it.location.physicalLocation.region.endColumn}" + } + ?: listOf(), + location = result.locations[0].physicalLocation.artifactLocation.uri, + ruleId = result.ruleId, + ruleName = result.rule?.id ?: "", + ruleDescription = result.message.text ?: "", + level = result.level.toString(), + kind = result.kind.toString(), + githubAlertNumber = additionalProperties["github/alertNumber"]?.toString() ?: "", + githubAlertUrl = additionalProperties["github/alertUrl"]?.toString() ?: "" ) return element } @@ -118,7 +156,7 @@ class SarifService { fun getPullRequests(github: GitHubInstance, repositoryFullName: String, branchName: String = "main"): List<*>? { val head = "${repositoryFullName.split("/")[0]}:$branchName" val connection = URL("${github.apiBase}/repos/$repositoryFullName/pulls?state=open&head=$head") - .openConnection() as HttpURLConnection + .openConnection() as HttpURLConnection connection.apply { requestMethod = "GET" @@ -139,14 +177,14 @@ class SarifService { } private fun getAnalysisFromGitHub( - github: GitHubInstance, - repositoryFullName: String, - branchName: String = "main" + github: GitHubInstance, + repositoryFullName: String, + branchName: String = "main" ): String { val s = "${github.apiBase}/repos/$repositoryFullName/code-scanning/analyses?ref=$branchName" val connection = URL(s) - .openConnection() as HttpURLConnection + .openConnection() as HttpURLConnection connection.apply { requestMethod = "GET" @@ -189,7 +227,7 @@ class SarifService { private fun getSarifFromGitHub(github: GitHubInstance, repositoryFullName: String, analysisId: Int): String { val connection = URL("${github.apiBase}/repos/$repositoryFullName/code-scanning/analyses/$analysisId") - .openConnection() as HttpURLConnection + .openConnection() as HttpURLConnection connection.apply { requestMethod = "GET" diff --git a/src/main/kotlin/com/github/adrienpessu/sarifviewer/toolWindow/SarifViewerWindowFactory.kt b/src/main/kotlin/com/github/adrienpessu/sarifviewer/toolWindow/SarifViewerWindowFactory.kt index 47043b7..2235e92 100644 --- a/src/main/kotlin/com/github/adrienpessu/sarifviewer/toolWindow/SarifViewerWindowFactory.kt +++ b/src/main/kotlin/com/github/adrienpessu/sarifviewer/toolWindow/SarifViewerWindowFactory.kt @@ -19,7 +19,10 @@ import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.ScrollType +import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.OpenFileDescriptor import com.intellij.openapi.project.DumbService @@ -30,6 +33,7 @@ import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.components.JBPanel +import com.intellij.ui.components.JBScrollPane import com.intellij.ui.components.JBTabbedPane import com.intellij.ui.content.ContentFactory import com.intellij.ui.table.JBTable @@ -155,8 +159,8 @@ class SarifViewerWindowFactory : ToolWindowFactory { } private fun JBPanel>.loadDataAndUI( - repository: GitRepository, - selectedCombo: BranchItemComboBox? = null + repository: GitRepository, + selectedCombo: BranchItemComboBox? = null ) { currentBranch = repository.currentBranch @@ -218,24 +222,24 @@ class SarifViewerWindowFactory : ToolWindowFactory { } private fun emptyNode( - map: HashMap>, - repositoryFullName: String? + map: MutableMap>, + repositoryFullName: String? ) { val element = Leaf( - leafName = "", - address = "", - steps = listOf(), - location = "", - ruleId = "", - ruleName = "", - ruleDescription = "", - level = "", - kind = "", - githubAlertNumber = "", - githubAlertUrl = "", + leafName = "", + address = "", + steps = listOf(), + location = "", + ruleId = "", + ruleName = "", + ruleDescription = "", + level = "", + kind = "", + githubAlertNumber = "", + githubAlertUrl = "", ) map["No SARIF file found for the repository $repositoryFullName and ref $sarifGitHubRef"] = - listOf(element).toMutableList() + listOf(element).toMutableList() } private fun toggleLoading(forcedValue: Boolean? = null) { @@ -249,9 +253,9 @@ class SarifViewerWindowFactory : ToolWindowFactory { errorField.text = message NotificationGroupManager.getInstance() - .getNotificationGroup("SARIF viewer") - .createNotification(message, NotificationType.ERROR) - .notify(project) + .getNotificationGroup("SARIF viewer") + .createNotification(message, NotificationType.ERROR) + .notify(project) thisLogger().info(message) } @@ -262,10 +266,11 @@ class SarifViewerWindowFactory : ToolWindowFactory { steps.add(tableSteps) // Add the table to a scroll pane - val scrollPane = JScrollPane(tableInfos) + val scrollPane = JBScrollPane(tableInfos) + val stepsScrollPane = JBScrollPane(steps) details.addTab("Infos", scrollPane) - details.addTab("Steps", steps) + details.addTab("Steps", stepsScrollPane) layout = BoxLayout(this, BoxLayout.Y_AXIS) @@ -301,7 +306,7 @@ class SarifViewerWindowFactory : ToolWindowFactory { toggleLoading() currentView = selectedItem clearJSplitPane() - var map = HashMap>() + var map: MutableMap> = HashMap() if (localMode) { if (cacheSarif?.runs?.isEmpty() == false) { map = service.analyseSarif(cacheSarif!!, currentView) @@ -329,7 +334,7 @@ class SarifViewerWindowFactory : ToolWindowFactory { comboBranchPR.addActionListener(ActionListener() { event -> val comboBox = event.source as JComboBox<*> if (event.actionCommand == "comboBoxChanged" && comboBox.selectedItem != null - && !disableComboBoxEvent && !DumbService.isDumb(project) + && !disableComboBoxEvent && !DumbService.isDumb(project) ) { val selectedOption = comboBox.selectedItem as BranchItemComboBox sarifGitHubRef = if (selectedOption.prNumber != 0) { @@ -371,7 +376,7 @@ class SarifViewerWindowFactory : ToolWindowFactory { } private fun buildContent( - map: HashMap> + map: Map> ) { treeBuilding(map) } @@ -391,9 +396,9 @@ class SarifViewerWindowFactory : ToolWindowFactory { } fun refresh( - currentBranch: GitLocalBranch, - github: GitHubInstance, - repositoryFullName: String + currentBranch: GitLocalBranch, + github: GitHubInstance, + repositoryFullName: String ) { localMode = false val worker = object : SwingWorker() { @@ -414,7 +419,7 @@ class SarifViewerWindowFactory : ToolWindowFactory { worker.execute() } - private fun treeBuilding(map: HashMap>) { + private fun treeBuilding(map: Map>) { val root = DefaultMutableTreeNode(project.name) map.forEach { (key, value) -> @@ -449,19 +454,19 @@ class SarifViewerWindowFactory : ToolWindowFactory { } val githubAlertUrl = leaf.githubAlertUrl - .replace("api.", "") - .replace("api/v3/", "") - .replace("repos/", "") - .replace("code-scanning/alerts", "security/code-scanning") + .replace("api.", "") + .replace("api/v3/", "") + .replace("repos/", "") + .replace("code-scanning/alerts", "security/code-scanning") tableInfos.clearSelection() // Create a table model with "Property" and "Value" columns val defaultTableModel: DefaultTableModel = - object : DefaultTableModel(arrayOf("Property", "Value"), 0) { - override fun isCellEditable(row: Int, column: Int): Boolean { - return false + object : DefaultTableModel(arrayOf("Property", "Value"), 0) { + override fun isCellEditable(row: Int, column: Int): Boolean { + return false + } } - } tableInfos.model = defaultTableModel // Add some data @@ -472,28 +477,28 @@ class SarifViewerWindowFactory : ToolWindowFactory { defaultTableModel.addRow(arrayOf("Location", leaf.location)) defaultTableModel.addRow(arrayOf("GitHub alert number", leaf.githubAlertNumber)) defaultTableModel.addRow( - arrayOf( - "GitHub alert url", - "$githubAlertUrl( + "GitHub alert url", + "$githubAlertUrl - FileEditorManager.getInstance(project).openTextEditor( - OpenFileDescriptor( - project, - virtualFile, - lineNumber - 1, - columnNumber - ), - true // request focus to editor - ) - val editor: Editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return - val inlayModel = editor.inlayModel + ?.let { virtualFile -> + FileEditorManager.getInstance(project).openTextEditor( + OpenFileDescriptor( + project, + virtualFile, + lineNumber - 1, + columnNumber + ), + true // request focus to editor + ) + val editor: Editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return + val inlayModel = editor.inlayModel - val offset = editor.document.getLineStartOffset(lineNumber - 1) + val offset = editor.document.getLineStartOffset(lineNumber - 1) - val icon = when (level) { - "error" -> "🛑" - "warning" -> "⚠️" - "note" -> "📝" - else -> "" - } - val description = "$icon $rule: $description" - if (description.isNotEmpty()) { - inlayModel.addBlockElement(offset, true, true, 1, MyCustomInlayRenderer(description)) + val icon = when (level) { + "error" -> "🛑" + "warning" -> "⚠️" + "note" -> "📝" + else -> "" + } + val description = "$icon $rule: $description" + if (description.isNotEmpty()) { + inlayModel.addBlockElement(offset, true, true, 1, MyCustomInlayRenderer(description)) + } } + } + + private fun getOffsetsFromLocation(document: Document, + startLine: Int, startColumn: Int, + endLine: Int = -1, endColumnNumber: Int = -1): Pair { + val startOffset = document.getLineStartOffset(startLine - 1) + startColumn - 1 + val endOffset: Int = if (endLine == -1) { + document.getLineEndOffset(startLine - 1) + } else { + if (endColumnNumber == -1) { + document.getLineEndOffset(endLine - 1) + } else { + document.getLineStartOffset(endLine - 1) + endColumnNumber - 1 } + } + return Pair(startOffset, endOffset) + } + + private fun navigateEditor( + project: Project, + path: String, + lineNumber: Int, + columnNumber: Int = 1, + endLineNumber: Int = -1, + endColumnNumber: Int = -1, + ) { + VirtualFileManager.getInstance().findFileByNioPath(Path.of("${project.basePath}/$path")) + ?.let { virtualFile -> + FileEditorManager.getInstance(project).openTextEditor( + OpenFileDescriptor( + project, + virtualFile, + lineNumber - 1, + columnNumber - 1 + ), + true // request focus to editor + ) + FileDocumentManager.getInstance().getDocument(virtualFile)?.let { document -> + val offsets = getOffsetsFromLocation(document, + lineNumber, columnNumber, + endLineNumber, endColumnNumber) + val editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return + editor.caretModel.moveToOffset(offsets.first) + editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE) + editor.selectionModel.setSelection(offsets.first, offsets.second) + + } + } } private fun clearJSplitPane() { @@ -630,12 +688,12 @@ class SarifViewerWindowFactory : ToolWindowFactory { } private fun extractSarif( - github: GitHubInstance, - repositoryFullName: String, - base: String? = null - ): HashMap> { + github: GitHubInstance, + repositoryFullName: String, + base: String? = null + ): MutableMap> { val sarifs = service.getSarifFromGitHub(github, repositoryFullName, sarifGitHubRef).filterNotNull() - var map = HashMap>() + var map: MutableMap> = HashMap() val results = sarifs.flatMap { it.runs?.get(0)?.results ?: emptyList() } if (sarifs.isNotEmpty()) { if (sarifGitHubRef.startsWith("refs/pull/") && base != null) { @@ -645,16 +703,16 @@ class SarifViewerWindowFactory : ToolWindowFactory { for (currentResult in results) { if (mainResults.none { - it.ruleId == currentResult.ruleId - && ("${currentResult.locations[0].physicalLocation.artifactLocation.uri}:${currentResult.locations[0].physicalLocation.region.startLine}" == "${it.locations[0].physicalLocation.artifactLocation.uri}:${it.locations[0].physicalLocation.region.startLine}") - }) { + it.ruleId == currentResult.ruleId + && ("${currentResult.locations[0].physicalLocation.artifactLocation.uri}:${currentResult.locations[0].physicalLocation.region.startLine}" == "${it.locations[0].physicalLocation.artifactLocation.uri}:${it.locations[0].physicalLocation.region.startLine}") + }) { resultsToDisplay.add(currentResult) } } map = service.analyseResult(resultsToDisplay) } else { map = sarifs.map { service.analyseSarif(it, currentView) } - .reduce { acc, item -> acc.apply { putAll(item) } } + .reduce { acc, item -> acc.apply { putAll(item) } } } } @@ -662,13 +720,13 @@ class SarifViewerWindowFactory : ToolWindowFactory { } private fun extractSarifFromFile( - file: File - ): HashMap> { + file: File + ): Map> { // file to String val sarifString = file.readText(Charset.defaultCharset()) val sarif = ObjectMapper().readValue(sarifString, SarifSchema210::class.java) cacheSarif = sarif - var map = HashMap>() + var map: MutableMap> = HashMap() if (sarif.runs?.isEmpty() == false) { map = service.analyseSarif(sarif, currentView) } @@ -677,26 +735,26 @@ class SarifViewerWindowFactory : ToolWindowFactory { } private fun populateCombo( - currentBranch: GitLocalBranch?, - github: GitHubInstance, - repositoryFullName: String + currentBranch: GitLocalBranch?, + github: GitHubInstance, + repositoryFullName: String ) { disableComboBoxEvent = true comboBranchPR.removeAllItems() comboBranchPR.addItem(BranchItemComboBox(0, currentBranch?.name ?: "main", "", "")) val pullRequests = - service.getPullRequests(github, repositoryFullName, sarifGitHubRef.split('/', limit = 3).last()) + service.getPullRequests(github, repositoryFullName, sarifGitHubRef.split('/', limit = 3).last()) if (pullRequests?.isNotEmpty() == true) { pullRequests.forEach { val currentPr = it as LinkedHashMap<*, *> comboBranchPR.addItem( - BranchItemComboBox( - currentPr["number"] as Int, - (currentPr["base"] as LinkedHashMap)["ref"] ?: "", - (currentPr["head"] as LinkedHashMap)["ref"] ?: "", - currentPr["title"].toString(), - (currentPr["head"] as LinkedHashMap)["commit_sha"] ?: "" - ) + BranchItemComboBox( + currentPr["number"] as Int, + (currentPr["base"] as LinkedHashMap)["ref"] ?: "", + (currentPr["head"] as LinkedHashMap)["ref"] ?: "", + currentPr["title"].toString(), + (currentPr["head"] as LinkedHashMap)["commit_sha"] ?: "" + ) ) } }