generated from leanix/repository-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16 from leanix/feature/CID-2777/secure-webhook-li…
…stener-endpoint CID-2777: Secure webhook listener endpoint
- Loading branch information
Showing
14 changed files
with
226 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
src/main/kotlin/net/leanix/githubagent/controllers/advice/GlobalExceptionHandler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package net.leanix.githubagent.controllers.advice | ||
|
||
import net.leanix.githubagent.exceptions.InvalidEventSignatureException | ||
import net.leanix.githubagent.exceptions.WebhookSecretNotSetException | ||
import org.slf4j.Logger | ||
import org.slf4j.LoggerFactory | ||
import org.springframework.http.HttpStatus | ||
import org.springframework.http.ProblemDetail | ||
import org.springframework.web.bind.annotation.ControllerAdvice | ||
import org.springframework.web.bind.annotation.ExceptionHandler | ||
|
||
@ControllerAdvice | ||
class GlobalExceptionHandler { | ||
|
||
val exceptionLogger: Logger = LoggerFactory.getLogger(GlobalExceptionHandler::class.java) | ||
|
||
@ExceptionHandler(InvalidEventSignatureException::class) | ||
fun handleInvalidEventSignatureException(exception: InvalidEventSignatureException): ProblemDetail { | ||
val problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, "Invalid event signature") | ||
problemDetail.title = exception.message | ||
exceptionLogger.warn(exception.message) | ||
return problemDetail | ||
} | ||
|
||
@ExceptionHandler(WebhookSecretNotSetException::class) | ||
fun handleWebhookSecretNotSetException(exception: WebhookSecretNotSetException): ProblemDetail { | ||
val problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Webhook secret not set") | ||
problemDetail.title = exception.message | ||
return problemDetail | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
src/main/kotlin/net/leanix/githubagent/services/GitHubWebhookHandler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package net.leanix.githubagent.services | ||
|
||
import net.leanix.githubagent.config.GitHubEnterpriseProperties | ||
import net.leanix.githubagent.exceptions.InvalidEventSignatureException | ||
import net.leanix.githubagent.exceptions.WebhookSecretNotSetException | ||
import net.leanix.githubagent.shared.SUPPORTED_EVENT_TYPES | ||
import net.leanix.githubagent.shared.hmacSHA256 | ||
import net.leanix.githubagent.shared.timingSafeEqual | ||
import org.slf4j.LoggerFactory | ||
import org.springframework.stereotype.Service | ||
|
||
@Service | ||
class GitHubWebhookHandler( | ||
private val webhookEventService: WebhookEventService, | ||
private val gitHubEnterpriseProperties: GitHubEnterpriseProperties | ||
) { | ||
|
||
private val logger = LoggerFactory.getLogger(GitHubWebhookHandler::class.java) | ||
|
||
fun handleWebhookEvent(eventType: String, host: String, signature256: String?, payload: String) { | ||
if (SUPPORTED_EVENT_TYPES.contains(eventType.uppercase())) { | ||
if (!gitHubEnterpriseProperties.baseUrl.contains(host)) { | ||
logger.error("Received a webhook event from an unknown host: $host") | ||
return | ||
} | ||
if (gitHubEnterpriseProperties.webhookSecret.isBlank() && signature256 != null) { | ||
logger.error( | ||
"Event signature is present but webhook secret is not set, " + | ||
"please restart the agent with a valid secret, " + | ||
"or remove the secret from the GitHub App settings." | ||
) | ||
throw WebhookSecretNotSetException() | ||
} | ||
if (gitHubEnterpriseProperties.webhookSecret.isNotBlank() && signature256 != null) { | ||
val hashedSecret = hmacSHA256(gitHubEnterpriseProperties.webhookSecret, payload) | ||
val isEqual = timingSafeEqual(signature256.removePrefix("sha256="), hashedSecret) | ||
if (!isEqual) throw InvalidEventSignatureException() | ||
} else { | ||
logger.warn("Webhook secret is not set, Skipping signature verification") | ||
} | ||
webhookEventService.consumeWebhookEvent(eventType, payload) | ||
} else { | ||
logger.warn("Received an unsupported event of type: $eventType") | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
src/main/kotlin/net/leanix/githubagent/shared/GitHubWebHookEventHelper.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package net.leanix.githubagent.shared | ||
|
||
import javax.crypto.Mac | ||
import javax.crypto.spec.SecretKeySpec | ||
|
||
fun hmacSHA256(secret: String, data: String): String { | ||
val secretKey = SecretKeySpec(secret.toByteArray(), "HmacSHA256") | ||
val mac = Mac.getInstance("HmacSHA256") | ||
mac.init(secretKey) | ||
val hmacData = mac.doFinal(data.toByteArray()) | ||
return hmacData.joinToString("") { "%02x".format(it) } | ||
} | ||
|
||
fun timingSafeEqual(a: String, b: String): Boolean { | ||
val aBytes = a.toByteArray() | ||
val bBytes = b.toByteArray() | ||
if (aBytes.size != bBytes.size) return false | ||
|
||
var diff = 0 | ||
for (i in aBytes.indices) { | ||
diff = diff or (aBytes[i].toInt() xor bBytes[i].toInt()) | ||
} | ||
return diff == 0 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
72 changes: 72 additions & 0 deletions
72
src/test/kotlin/net/leanix/githubagent/services/GitHubWebhookHandlerTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package net.leanix.githubagent.services | ||
|
||
import io.mockk.every | ||
import io.mockk.mockk | ||
import io.mockk.verify | ||
import net.leanix.githubagent.config.GitHubEnterpriseProperties | ||
import net.leanix.githubagent.exceptions.InvalidEventSignatureException | ||
import net.leanix.githubagent.exceptions.WebhookSecretNotSetException | ||
import org.junit.jupiter.api.BeforeEach | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.assertThrows | ||
|
||
class GitHubWebhookHandlerTest { | ||
|
||
private val webhookEventService = mockk<WebhookEventService>() | ||
private val gitHubEnterpriseProperties = mockk<GitHubEnterpriseProperties>() | ||
private val gitHubWebhookHandler = GitHubWebhookHandler(webhookEventService, gitHubEnterpriseProperties) | ||
|
||
@BeforeEach | ||
fun setUp() { | ||
} | ||
|
||
@Test | ||
fun `should not process event if unknown host`() { | ||
every { gitHubEnterpriseProperties.baseUrl } returns "known.host" | ||
|
||
gitHubWebhookHandler.handleWebhookEvent("PUSH", "unknown.host", null, "{}") | ||
|
||
verify(exactly = 0) { webhookEventService.consumeWebhookEvent(any(), any()) } | ||
} | ||
|
||
@Test | ||
fun `should throw WebhookSecretNotSetException when signature is present but secret is not set`() { | ||
every { gitHubEnterpriseProperties.baseUrl } returns "known.host" | ||
every { gitHubEnterpriseProperties.webhookSecret } returns "" | ||
|
||
assertThrows<WebhookSecretNotSetException> { | ||
gitHubWebhookHandler.handleWebhookEvent("PUSH", "known.host", "sha256=signature", "{}") | ||
} | ||
} | ||
|
||
@Test | ||
fun `should throw InvalidEventSignatureException for invalid signature`() { | ||
every { gitHubEnterpriseProperties.baseUrl } returns "known.host" | ||
every { gitHubEnterpriseProperties.webhookSecret } returns "secret" | ||
|
||
assertThrows<InvalidEventSignatureException> { | ||
gitHubWebhookHandler.handleWebhookEvent("PUSH", "known.host", "sha256=signature", "{}") | ||
} | ||
} | ||
|
||
@Test | ||
fun `should not process unsupported event type`() { | ||
every { gitHubEnterpriseProperties.baseUrl } returns "known.host" | ||
every { gitHubEnterpriseProperties.webhookSecret } returns "" | ||
|
||
gitHubWebhookHandler.handleWebhookEvent("UNSUPPORTED_EVENT", "known.host", null, "{}") | ||
|
||
verify(exactly = 0) { webhookEventService.consumeWebhookEvent(any(), any()) } | ||
} | ||
|
||
@Test | ||
fun `should process supported event type successfully`() { | ||
every { gitHubEnterpriseProperties.baseUrl } returns "host" | ||
every { gitHubEnterpriseProperties.webhookSecret } returns "" | ||
every { webhookEventService.consumeWebhookEvent(any(), any()) } returns Unit | ||
|
||
gitHubWebhookHandler.handleWebhookEvent("PUSH", "host", null, "{}") | ||
|
||
verify { webhookEventService.consumeWebhookEvent("PUSH", "{}") } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters