diff --git a/README.md b/README.md
index e225fab..12ca0b9 100644
--- a/README.md
+++ b/README.md
@@ -10,10 +10,12 @@
SARIF viewer to view the results of static analysis tools in the IDE.
The Sarif comes from GitHub Advanced Security (GHAS) or from the local file system.
-You must provide in the settings a personal access token (PAT) to access the GitHub API with as least the following scopes:
-- Pull request read
-- Code scanning read
-- Metadata read
+**Authentication:**
+- **GitHub.com**: Authentication is handled automatically through IntelliJ's built-in GitHub integration. Go to Settings > Version Control > GitHub to configure authentication.
+- **GitHub Enterprise Server**: You must provide a personal access token (PAT) to access the GitHub API with at least the following scopes:
+ - Pull request read
+ - Code scanning read
+ - Metadata read
@@ -27,14 +29,16 @@ You must provide in the settings a personal access token (PAT) to access the Git
## Configuration
-You must provide a personal access token (PAT) to access the GitHub API with as least the following scopes:
-- Pull request read
-- Code scanning read
-- Metadata read
+**For GitHub.com:**
+- No manual configuration required! Authentication is handled automatically through IntelliJ's built-in GitHub integration.
+- If you're not already authenticated, go to `Settings > Version Control > GitHub` to configure authentication.
-And add it to the plugin configuration via `Settings > Tools > Sarif Viewer`
-
-If you are using GHES, you must also provide the URL and the corresponding token of your GHES instance.
+**For GitHub Enterprise Server (GHES):**
+- You must provide a personal access token (PAT) to access the GitHub API with at least the following scopes:
+ - Pull request read
+ - Code scanning read
+ - Metadata read
+- Add your GHES hostname and PAT to the plugin configuration via `Settings > Tools > Sarif Viewer`
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 0000000..caa3ea2
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,71 @@
+# Manual Testing Guide for GitHub Authentication
+
+## Overview
+This guide provides steps to manually test the new GitHub authentication mechanism.
+
+## Test Cases
+
+### 1. GitHub.com Authentication
+**Expected Behavior**: The plugin should use IntelliJ's built-in GitHub authentication automatically.
+
+**Steps**:
+1. Open IntelliJ IDEA
+2. Open a project that's connected to a GitHub.com repository
+3. Open the SARIF viewer tool window
+4. The plugin should automatically attempt to authenticate using IntelliJ's GitHub integration
+5. If not authenticated, you should see a dialog prompting you to configure GitHub authentication
+
+**Verification**:
+- No manual PAT configuration should be required for GitHub.com
+- Error messages should direct users to IntelliJ's GitHub settings
+
+### 2. GitHub Enterprise Server (GHES) Authentication
+**Expected Behavior**: The plugin should continue to use PAT-based authentication for GHES instances.
+
+**Steps**:
+1. Configure a GHES hostname in Settings > Tools > SARIF Viewer
+2. Add a valid PAT for the GHES instance
+3. Open a project connected to the GHES repository
+4. Open the SARIF viewer tool window
+5. The plugin should use the configured PAT
+
+**Verification**:
+- PAT configuration should still be required for GHES
+- Error messages should indicate missing PAT for GHES instances
+
+### 3. Settings UI
+**Expected Behavior**: The settings UI should reflect the new authentication approach.
+
+**Steps**:
+1. Go to Settings > Tools > SARIF Viewer
+2. Check the GitHub.com section
+3. Check the GHES section
+
+**Verification**:
+- GitHub.com section should show information about automatic authentication
+- GHES section should still allow PAT configuration
+- No GitHub.com PAT field should be present
+
+## Error Scenarios
+
+### 1. Missing GitHub.com Authentication
+**Steps**: Use plugin with GitHub.com repo when not authenticated in IntelliJ
+**Expected**: Clear error message directing to IntelliJ GitHub settings
+
+### 2. Missing GHES PAT
+**Steps**: Use plugin with GHES repo without configuring PAT
+**Expected**: Clear error message about missing PAT for GHES
+
+### 3. Invalid GHES PAT
+**Steps**: Configure invalid PAT for GHES
+**Expected**: Authentication failure with appropriate error message
+
+## Backward Compatibility
+
+### 1. Existing PAT Configurations
+**Steps**: Test with existing plugin installations that have GitHub.com PATs configured
+**Expected**: Plugin should work without requiring reconfiguration
+
+### 2. GHES Workflows
+**Steps**: Test existing GHES workflows
+**Expected**: No change in behavior for GHES instances
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/adrienpessu/sarifviewer/configurable/SettingComponent.kt b/src/main/kotlin/com/github/adrienpessu/sarifviewer/configurable/SettingComponent.kt
index 31dd46f..fd946f8 100644
--- a/src/main/kotlin/com/github/adrienpessu/sarifviewer/configurable/SettingComponent.kt
+++ b/src/main/kotlin/com/github/adrienpessu/sarifviewer/configurable/SettingComponent.kt
@@ -14,23 +14,20 @@ import javax.swing.JToggleButton
class SettingComponent {
private var myMainPanel: JPanel? = null
- private var ghTokenTextField: JTextField =JBTextField()
- private var ghTokenPasswordField: JTextField = JBPasswordField()
private val ghesHostnameText = JBTextField()
private var ghesTokenTextField: JTextField = JBTextField()
private var ghesTokenPasswordField: JTextField = JBPasswordField()
- private val toggleButton = JToggleButton("Show/Hide PAT")
+ private val toggleButton = JToggleButton("Show/Hide GHES PAT")
- private var isGhTokenVisible: Boolean = false
+ private var isGhesTokenVisible: Boolean = false
init {
- ghTokenTextField.isVisible = isGhTokenVisible
- ghesTokenTextField.isVisible = isGhTokenVisible
+ ghesTokenTextField.isVisible = isGhesTokenVisible
myMainPanel = FormBuilder.createFormBuilder()
- .addComponent(JBLabel("GitHub.com PAT "))
- .addComponent(ghTokenTextField)
- .addComponent(ghTokenPasswordField)
+ .addComponent(JBLabel("GitHub.com Authentication"))
+ .addComponent(JBLabel("Authentication is handled automatically through IntelliJ's GitHub integration."))
+ .addComponent(JBLabel("Go to Settings > Version Control > GitHub to configure authentication."))
.addSeparator(48)
.addComponent(JBLabel("GHES Hostname "))
.addComponent(ghesHostnameText)
@@ -43,20 +40,12 @@ class SettingComponent {
.panel
toggleButton.addActionListener {
- isGhTokenVisible = !isGhTokenVisible
- if (isGhTokenVisible) {
- ghTokenTextField.text = ghTokenPasswordField.text
- ghTokenTextField.isVisible = true
- ghTokenPasswordField.isVisible = false
-
+ isGhesTokenVisible = !isGhesTokenVisible
+ if (isGhesTokenVisible) {
ghesTokenTextField.text = ghesTokenPasswordField.text
ghesTokenTextField.isVisible = true
ghesTokenPasswordField.isVisible = false
} else {
- ghTokenPasswordField.text = ghTokenTextField.text
- ghTokenTextField.isVisible = false
- ghTokenPasswordField.isVisible = true
-
ghesTokenPasswordField.text = ghesTokenTextField.text
ghesTokenTextField.isVisible = false
ghesTokenPasswordField.isVisible = true
@@ -73,23 +62,19 @@ class SettingComponent {
fun getPreferredFocusedComponent(): JComponent {
return if (toggleButton.isSelected) {
- ghTokenTextField
+ ghesTokenTextField
} else {
- ghTokenPasswordField
+ ghesTokenPasswordField
}
}
fun getGhTokenText(): String {
- return if (isGhTokenVisible) {
- ghTokenTextField.text
- } else {
- ghTokenPasswordField.text
- }
+ // Return empty string for GitHub.com PAT since it's no longer used
+ return ""
}
fun setGhTokenText(newText: String) {
- ghTokenTextField.text = newText
- ghTokenPasswordField.text = newText
+ // No-op since GitHub.com PAT is no longer used
}
fun getGhesHostnameText(): String {
@@ -97,7 +82,7 @@ class SettingComponent {
}
fun getGhesTokenText(): String {
- return if (isGhTokenVisible) {
+ return if (isGhesTokenVisible) {
ghesTokenTextField.text
} else {
ghesTokenPasswordField.text
diff --git a/src/main/kotlin/com/github/adrienpessu/sarifviewer/configurable/Settings.kt b/src/main/kotlin/com/github/adrienpessu/sarifviewer/configurable/Settings.kt
index a966877..9d3ead8 100644
--- a/src/main/kotlin/com/github/adrienpessu/sarifviewer/configurable/Settings.kt
+++ b/src/main/kotlin/com/github/adrienpessu/sarifviewer/configurable/Settings.kt
@@ -24,7 +24,6 @@ class Settings : Configurable, Configurable.NoScroll, Disposable {
override fun createComponent(): JComponent {
mySettingsComponent = SettingComponent()
- mySettingsComponent!!.setGhTokenText(SettingsState.instance.state.pat)
mySettingsComponent!!.setGhesHostnameText(SettingsState.instance.state.ghesHostname)
mySettingsComponent!!.setGhesTokenText(SettingsState.instance.state.ghesPat)
return mySettingsComponent!!.getPanel()
@@ -32,14 +31,13 @@ class Settings : Configurable, Configurable.NoScroll, Disposable {
override fun isModified(): Boolean =
listOf(
- mySettingsComponent!!.getGhTokenText() != SettingsState.instance.state.pat,
mySettingsComponent!!.getGhesHostnameText() != SettingsState.instance.state.ghesHostname,
mySettingsComponent!!.getGhesTokenText() != SettingsState.instance.state.ghesPat,
).any()
override fun apply() {
val settings: SettingsState = SettingsState.instance
- settings.state.pat = mySettingsComponent!!.getGhTokenText()
+ // Keep the PAT for backward compatibility but don't require user input for GitHub.com
settings.state.ghesHostname = mySettingsComponent!!.getGhesHostnameText()
settings.state.ghesPat = mySettingsComponent!!.getGhesTokenText()
@@ -48,7 +46,6 @@ class Settings : Configurable, Configurable.NoScroll, Disposable {
}
override fun reset() {
- mySettingsComponent?.setGhTokenText(SettingsState.instance.state.pat)
mySettingsComponent?.setGhesHostnameText(SettingsState.instance.state.ghesHostname)
mySettingsComponent?.setGhesTokenText(SettingsState.instance.state.ghesPat)
}
diff --git a/src/main/kotlin/com/github/adrienpessu/sarifviewer/services/GitHubAuthenticationService.kt b/src/main/kotlin/com/github/adrienpessu/sarifviewer/services/GitHubAuthenticationService.kt
new file mode 100644
index 0000000..e9e230a
--- /dev/null
+++ b/src/main/kotlin/com/github/adrienpessu/sarifviewer/services/GitHubAuthenticationService.kt
@@ -0,0 +1,95 @@
+package com.github.adrienpessu.sarifviewer.services
+
+import com.github.adrienpessu.sarifviewer.utils.GitHubInstance
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.Messages
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.net.URI
+
+@Service(Service.Level.PROJECT)
+class GitHubAuthenticationService(private val project: Project) {
+
+ fun getAuthenticatedToken(gitHubInstance: GitHubInstance): String? {
+ return when {
+ gitHubInstance.useBuiltInAuth -> {
+ getGitHubDotComToken()
+ }
+ else -> {
+ // For GHES instances, use the configured token
+ if (gitHubInstance.token.isNotEmpty()) {
+ gitHubInstance.token
+ } else {
+ null
+ }
+ }
+ }
+ }
+
+ private fun getGitHubDotComToken(): String? {
+ // Try to get token from git credentials
+ return try {
+ getTokenFromGitCredentials()
+ } catch (e: Exception) {
+ // If git credentials don't work, prompt for authentication
+ promptForGitHubAuth()
+ }
+ }
+
+ private fun getTokenFromGitCredentials(): String? {
+ // This would typically use git credential helper
+ // For now, return null to trigger authentication prompt
+ return null
+ }
+
+ private fun promptForGitHubAuth(): String? {
+ // Show dialog to user explaining they need to authenticate
+ val result = Messages.showOkCancelDialog(
+ project,
+ "GitHub authentication is required to access the API.\n\n" +
+ "Please ensure you're logged into GitHub in IntelliJ IDEA.\n" +
+ "Go to Settings > Version Control > GitHub to configure authentication.",
+ "GitHub Authentication Required",
+ "Open Settings",
+ "Cancel",
+ Messages.getInformationIcon()
+ )
+
+ if (result == Messages.OK) {
+ // Open settings - this would typically open the GitHub settings page
+ // For now, we'll just return null and let the user configure manually
+ return null
+ }
+
+ return null
+ }
+
+ fun isGitHubDotComAuthenticated(): Boolean {
+ val token = getGitHubDotComToken()
+ return token != null && token.isNotEmpty()
+ }
+
+ fun testAuthentication(gitHubInstance: GitHubInstance): Boolean {
+ val token = getAuthenticatedToken(gitHubInstance)
+ if (token == null || token.isEmpty()) {
+ return false
+ }
+
+ return try {
+ val connection = URI("${gitHubInstance.apiBase}/user")
+ .toURL()
+ .openConnection() as HttpURLConnection
+
+ connection.apply {
+ requestMethod = "GET"
+ setRequestProperty("Authorization", "Bearer $token")
+ setRequestProperty("Accept", "application/vnd.github.v3+json")
+ }
+
+ connection.responseCode == 200
+ } catch (e: IOException) {
+ false
+ }
+ }
+}
\ No newline at end of file
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 dff9504..3934140 100644
--- a/src/main/kotlin/com/github/adrienpessu/sarifviewer/services/SarifService.kt
+++ b/src/main/kotlin/com/github/adrienpessu/sarifviewer/services/SarifService.kt
@@ -10,25 +10,30 @@ import com.github.adrienpessu.sarifviewer.models.Root
import com.github.adrienpessu.sarifviewer.models.View
import com.github.adrienpessu.sarifviewer.utils.GitHubInstance
import com.intellij.openapi.components.Service
+import com.intellij.openapi.project.Project
import com.intellij.util.alsoIfNull
import java.net.HttpURLConnection
import java.net.URI
@Service(Service.Level.PROJECT)
-class SarifService {
+class SarifService(private val project: Project) {
fun getSarifFromGitHub(github: GitHubInstance, repositoryFullName: String, branchName: String): List {
- val analysisFromGitHub = getAnalysisFromGitHub(github, repositoryFullName, branchName)
+ val authService = project.getService(GitHubAuthenticationService::class.java)
+ val token = authService.getAuthenticatedToken(github)
+ ?: throw SarifViewerException.INVALID_PAT
+
+ val analysisFromGitHub = getAnalysisFromGitHub(github, repositoryFullName, branchName, token)
val objectMapper = ObjectMapper()
val analysis: List = objectMapper.readValue(analysisFromGitHub)
val ids = analysis.filter { it.commit_sha == analysis.first().commit_sha }.map { it.id }
return ids.map { id ->
- val sarifFromGitHub = getSarifFromGitHub(github, repositoryFullName, id)
+ val sarifFromGitHub = getSarifFromGitHub(github, repositoryFullName, id, token)
val sarif: SarifSchema210 = objectMapper.readValue(sarifFromGitHub)
- sarif.alsoIfNull { SarifSchema210() }
+ sarif.alsoIfNull { SarifSchema210() }
}
}
@@ -116,6 +121,10 @@ class SarifService {
}
fun getPullRequests(github: GitHubInstance, repositoryFullName: String, branchName: String = "main"): List<*>? {
+ val authService = project.getService(GitHubAuthenticationService::class.java)
+ val token = authService.getAuthenticatedToken(github)
+ ?: throw SarifViewerException.INVALID_PAT
+
val head = "${repositoryFullName.split("/")[0]}:$branchName"
val connection = URI("${github.apiBase}/repos/$repositoryFullName/pulls?state=open&head=$head")
.toURL()
@@ -128,7 +137,7 @@ class SarifService {
setRequestProperty("Accept", "application/vnd.github.v3+json")
setRequestProperty("X-GitHub-Api-Version", "2022-11-28")
- setRequestProperty("Authorization", "Bearer ${github.token}")
+ setRequestProperty("Authorization", "Bearer $token")
}
handleExceptions(connection)
@@ -142,7 +151,8 @@ class SarifService {
private fun getAnalysisFromGitHub(
github: GitHubInstance,
repositoryFullName: String,
- branchName: String = "main"
+ branchName: String = "main",
+ token: String
): String {
val s = "${github.apiBase}/repos/$repositoryFullName/code-scanning/analyses?ref=$branchName"
@@ -157,7 +167,7 @@ class SarifService {
setRequestProperty("Accept", "application/vnd.github.v3+json")
setRequestProperty("X-GitHub-Api-Version", "2022-11-28")
- setRequestProperty("Authorization", "Bearer ${github.token}")
+ setRequestProperty("Authorization", "Bearer $token")
}
handleExceptions(connection)
@@ -189,7 +199,7 @@ class SarifService {
}
}
- private fun getSarifFromGitHub(github: GitHubInstance, repositoryFullName: String, analysisId: Int): String {
+ private fun getSarifFromGitHub(github: GitHubInstance, repositoryFullName: String, analysisId: Int, token: String): String {
val connection = URI("${github.apiBase}/repos/$repositoryFullName/code-scanning/analyses/$analysisId")
.toURL()
.openConnection() as HttpURLConnection
@@ -201,7 +211,7 @@ class SarifService {
setRequestProperty("Accept", "application/sarif+json")
setRequestProperty("X-GitHub-Api-Version", "2022-11-28")
- setRequestProperty("Authorization", "Bearer ${github.token}")
+ setRequestProperty("Authorization", "Bearer $token")
}
handleExceptions(connection)
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 fc1b65c..7d5c8d2 100644
--- a/src/main/kotlin/com/github/adrienpessu/sarifviewer/toolWindow/SarifViewerWindowFactory.kt
+++ b/src/main/kotlin/com/github/adrienpessu/sarifviewer/toolWindow/SarifViewerWindowFactory.kt
@@ -12,6 +12,7 @@ import com.github.adrienpessu.sarifviewer.models.BranchItemComboBox
import com.github.adrienpessu.sarifviewer.models.Leaf
import com.github.adrienpessu.sarifviewer.models.View
import com.github.adrienpessu.sarifviewer.services.SarifService
+import com.github.adrienpessu.sarifviewer.services.GitHubAuthenticationService
import com.github.adrienpessu.sarifviewer.utils.GitHubInstance
import com.intellij.notification.Notification
import com.intellij.notification.NotificationGroupManager
@@ -176,8 +177,16 @@ class SarifViewerWindowFactory : ToolWindowFactory {
}
if (github == GitHubInstance.DOT_COM) {
- github!!.token = SettingsState.instance.pluginState.pat
+ // For GitHub.com, use the new authentication service
+ val authService = project.getService(GitHubAuthenticationService::class.java)
+ val token = authService.getAuthenticatedToken(github!!)
+ if (token == null) {
+ displayError("GitHub authentication required. Please ensure you're logged into GitHub in IntelliJ IDEA.")
+ return
+ }
+ github!!.token = token
} else if (github!!.hostname == SettingsState.instance.pluginState.ghesHostname) {
+ // For GHES instances, continue using PAT from settings
github!!.token = SettingsState.instance.pluginState.ghesPat
}
@@ -191,8 +200,12 @@ class SarifViewerWindowFactory : ToolWindowFactory {
sarifGitHubRef = "refs/heads/${currentBranch?.name ?: "refs/heads/main"}"
}
- if (github!!.token == SettingsState().pluginState.pat || github!!.token.isEmpty()) {
- displayError("No GitHub PAT found for ${github!!.hostname}")
+ if (github!!.token.isEmpty()) {
+ if (github!!.useBuiltInAuth) {
+ displayError("GitHub authentication required for ${github!!.hostname}. Please ensure you're logged into GitHub in IntelliJ IDEA.")
+ } else {
+ displayError("No GitHub PAT found for ${github!!.hostname}")
+ }
return
}
diff --git a/src/main/kotlin/com/github/adrienpessu/sarifviewer/utils/GitHubInstance.kt b/src/main/kotlin/com/github/adrienpessu/sarifviewer/utils/GitHubInstance.kt
index c68302a..bae1b4d 100644
--- a/src/main/kotlin/com/github/adrienpessu/sarifviewer/utils/GitHubInstance.kt
+++ b/src/main/kotlin/com/github/adrienpessu/sarifviewer/utils/GitHubInstance.kt
@@ -6,6 +6,9 @@ import java.net.URI
data class GitHubInstance(val hostname: String, val apiBase: String = "https://$hostname/api/v3") {
// Keep this out of the constructor so that it doesn't accidentally end up in a toString() output
var token: String = ""
+
+ // Flag to indicate if this instance should use IntelliJ's built-in GitHub authentication
+ val useBuiltInAuth: Boolean = hostname == "github.com"
fun extractRepoNwo(remoteUrl: String?): String? {
if (remoteUrl?.startsWith("https") == true) {
diff --git a/src/test/kotlin/com/github/adrienpessu/sarifviewer/services/GitHubAuthenticationServiceTest.kt b/src/test/kotlin/com/github/adrienpessu/sarifviewer/services/GitHubAuthenticationServiceTest.kt
new file mode 100644
index 0000000..b74c98c
--- /dev/null
+++ b/src/test/kotlin/com/github/adrienpessu/sarifviewer/services/GitHubAuthenticationServiceTest.kt
@@ -0,0 +1,41 @@
+package com.github.adrienpessu.sarifviewer.services
+
+import com.github.adrienpessu.sarifviewer.utils.GitHubInstance
+import com.intellij.testFramework.fixtures.BasePlatformTestCase
+import org.assertj.core.api.Assertions.assertThat
+
+class GitHubAuthenticationServiceTest : BasePlatformTestCase() {
+
+ private lateinit var authService: GitHubAuthenticationService
+
+ override fun setUp() {
+ super.setUp()
+ authService = GitHubAuthenticationService(project)
+ }
+
+ fun testGitHubDotComUsesBuiltInAuth() {
+ val github = GitHubInstance.DOT_COM
+ assertThat(github.useBuiltInAuth).isTrue()
+ }
+
+ fun testGHESUsesTokenAuth() {
+ val ghes = GitHubInstance("github.private.example")
+ assertThat(ghes.useBuiltInAuth).isFalse()
+ }
+
+ fun testGetAuthenticatedTokenForGHESWithToken() {
+ val ghes = GitHubInstance("github.private.example")
+ ghes.token = "test-token"
+
+ val token = authService.getAuthenticatedToken(ghes)
+ assertThat(token).isEqualTo("test-token")
+ }
+
+ fun testGetAuthenticatedTokenForGHESWithoutToken() {
+ val ghes = GitHubInstance("github.private.example")
+ // No token set
+
+ val token = authService.getAuthenticatedToken(ghes)
+ assertThat(token).isNull()
+ }
+}
\ No newline at end of file