Browse Source

chore: Webhooks fixes

jamesread 6 months ago
parent
commit
c714dc0c62

+ 54 - 19
service/internal/webhooks/auth.go

@@ -22,23 +22,47 @@ func NewAuthVerifier(cfg config.WebhookConfig) *AuthVerifier {
 }
 }
 
 
 func (v *AuthVerifier) Verify(r *http.Request, payload []byte) bool {
 func (v *AuthVerifier) Verify(r *http.Request, payload []byte) bool {
-	switch v.config.AuthType {
-	case "hmac-sha256":
-		return v.verifyHMAC256(r, payload)
-	case "hmac-sha1":
-		return v.verifyHMAC1(r, payload)
-	case "bearer":
-		return v.verifyBearer(r)
-	case "basic":
-		return v.verifyBasic(r)
-	case "none", "":
-		return true
-	default:
-		log.WithFields(log.Fields{
-			"authType": v.config.AuthType,
-		}).Warnf("Unknown auth type, rejecting")
-		return false
+	verifier := v.getVerifier()
+	if verifier == nil {
+		return v.handleUnknownAuthType()
 	}
 	}
+	return verifier(r, payload)
+}
+
+type authVerifierFunc func(*http.Request, []byte) bool
+
+func (v *AuthVerifier) getVerifier() authVerifierFunc {
+	if v.config.AuthType == "" || v.config.AuthType == "none" {
+		return func(_ *http.Request, _ []byte) bool {
+			return true
+		}
+	}
+
+	verifierMap := v.buildVerifierMap()
+	if verifier, ok := verifierMap[v.config.AuthType]; ok {
+		return verifier
+	}
+	return nil
+}
+
+func (v *AuthVerifier) buildVerifierMap() map[string]authVerifierFunc {
+	return map[string]authVerifierFunc{
+		"hmac-sha256": v.verifyHMAC256,
+		"hmac-sha1":   v.verifyHMAC1,
+		"bearer": func(r *http.Request, _ []byte) bool {
+			return v.verifyBearer(r)
+		},
+		"basic": func(r *http.Request, _ []byte) bool {
+			return v.verifyBasic(r)
+		},
+	}
+}
+
+func (v *AuthVerifier) handleUnknownAuthType() bool {
+	log.WithFields(log.Fields{
+		"authType": v.config.AuthType,
+	}).Warnf("Unknown auth type, rejecting")
+	return false
 }
 }
 
 
 func (v *AuthVerifier) verifyHMAC256(r *http.Request, payload []byte) bool {
 func (v *AuthVerifier) verifyHMAC256(r *http.Request, payload []byte) bool {
@@ -123,12 +147,23 @@ func (v *AuthVerifier) verifyBasic(r *http.Request) bool {
 		return false
 		return false
 	}
 	}
 
 
+	return v.verifyBasicCredentials(username, password)
+}
+
+func (v *AuthVerifier) verifyBasicCredentials(username, password string) bool {
 	parts := strings.SplitN(v.config.Secret, ":", 2)
 	parts := strings.SplitN(v.config.Secret, ":", 2)
 	if len(parts) == 2 {
 	if len(parts) == 2 {
-		usernameMatch := subtle.ConstantTimeCompare([]byte(username), []byte(parts[0]))
-		passwordMatch := subtle.ConstantTimeCompare([]byte(password), []byte(parts[1]))
-		return usernameMatch == 1 && passwordMatch == 1
+		return v.verifyBasicWithUsername(username, password, parts[0], parts[1])
 	}
 	}
+	return v.verifyBasicPasswordOnly(password)
+}
+
+func (v *AuthVerifier) verifyBasicWithUsername(username, password, expectedUsername, expectedPassword string) bool {
+	usernameMatch := subtle.ConstantTimeCompare([]byte(username), []byte(expectedUsername))
+	passwordMatch := subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword))
+	return usernameMatch == 1 && passwordMatch == 1
+}
 
 
+func (v *AuthVerifier) verifyBasicPasswordOnly(password string) bool {
 	return subtle.ConstantTimeCompare([]byte(password), []byte(v.config.Secret)) == 1
 	return subtle.ConstantTimeCompare([]byte(password), []byte(v.config.Secret)) == 1
 }
 }

+ 76 - 128
service/internal/webhooks/github.go

@@ -7,158 +7,106 @@ import (
 // ApplyGitHubTemplate applies GitHub-specific template configurations
 // ApplyGitHubTemplate applies GitHub-specific template configurations
 // This allows users to use simple template names instead of configuring everything manually
 // This allows users to use simple template names instead of configuring everything manually
 func ApplyGitHubTemplate(cfg *config.WebhookConfig, template string) {
 func ApplyGitHubTemplate(cfg *config.WebhookConfig, template string) {
-	switch template {
-	case "github-push":
-		applyGitHubPushTemplate(cfg)
-	case "github-pr", "github-pull-request":
-		applyGitHubPRTemplate(cfg)
-	case "github-release":
-		applyGitHubReleaseTemplate(cfg)
-	case "github-workflow":
-		applyGitHubWorkflowTemplate(cfg)
+	applier := getTemplateApplier(template)
+	if applier != nil {
+		applier(cfg)
 	}
 	}
 }
 }
 
 
-func applyGitHubPushTemplate(cfg *config.WebhookConfig) {
+type templateApplier func(*config.WebhookConfig)
+
+func getTemplateApplier(template string) templateApplier {
+	templateMap := map[string]templateApplier{
+		"github-push":         applyGitHubPushTemplate,
+		"github-pr":           applyGitHubPRTemplate,
+		"github-pull-request": applyGitHubPRTemplate,
+		"github-release":      applyGitHubReleaseTemplate,
+		"github-workflow":     applyGitHubWorkflowTemplate,
+	}
+	return templateMap[template]
+}
+
+func setDefaultAuth(cfg *config.WebhookConfig) {
 	if cfg.AuthHeader == "" {
 	if cfg.AuthHeader == "" {
 		cfg.AuthHeader = "X-Hub-Signature-256"
 		cfg.AuthHeader = "X-Hub-Signature-256"
 	}
 	}
 	if cfg.AuthType == "" {
 	if cfg.AuthType == "" {
 		cfg.AuthType = "hmac-sha256"
 		cfg.AuthType = "hmac-sha256"
 	}
 	}
+}
+
+func ensureMatchHeaders(cfg *config.WebhookConfig) {
 	if len(cfg.MatchHeaders) == 0 {
 	if len(cfg.MatchHeaders) == 0 {
 		cfg.MatchHeaders = make(map[string]string)
 		cfg.MatchHeaders = make(map[string]string)
 	}
 	}
-	if _, exists := cfg.MatchHeaders["X-GitHub-Event"]; !exists {
-		cfg.MatchHeaders["X-GitHub-Event"] = "push"
-	}
+}
+
+func ensureExtract(cfg *config.WebhookConfig) {
 	if len(cfg.Extract) == 0 {
 	if len(cfg.Extract) == 0 {
 		cfg.Extract = make(map[string]string)
 		cfg.Extract = make(map[string]string)
 	}
 	}
-	if _, exists := cfg.Extract["git_repository"]; !exists {
-		cfg.Extract["git_repository"] = "$.repository.full_name"
-	}
-	if _, exists := cfg.Extract["git_ref"]; !exists {
-		cfg.Extract["git_ref"] = "$.ref"
-	}
-	if _, exists := cfg.Extract["git_commit"]; !exists {
-		cfg.Extract["git_commit"] = "$.head_commit.id"
-	}
-	if _, exists := cfg.Extract["git_branch"]; !exists {
-		cfg.Extract["git_branch"] = "$.ref"
-	}
-	if _, exists := cfg.Extract["git_message"]; !exists {
-		cfg.Extract["git_message"] = "$.head_commit.message"
+}
+
+func setExtractIfMissing(cfg *config.WebhookConfig, key, value string) {
+	if _, exists := cfg.Extract[key]; !exists {
+		cfg.Extract[key] = value
 	}
 	}
-	if _, exists := cfg.Extract["git_author"]; !exists {
-		cfg.Extract["git_author"] = "$.head_commit.author.name"
+}
+
+func setMatchHeaderIfMissing(cfg *config.WebhookConfig, key, value string) {
+	if _, exists := cfg.MatchHeaders[key]; !exists {
+		cfg.MatchHeaders[key] = value
 	}
 	}
 }
 }
 
 
+func applyGitHubPushTemplate(cfg *config.WebhookConfig) {
+	setDefaultAuth(cfg)
+	ensureMatchHeaders(cfg)
+	setMatchHeaderIfMissing(cfg, "X-GitHub-Event", "push")
+	ensureExtract(cfg)
+	setExtractIfMissing(cfg, "git_repository", "$.repository.full_name")
+	setExtractIfMissing(cfg, "git_ref", "$.ref")
+	setExtractIfMissing(cfg, "git_commit", "$.head_commit.id")
+	setExtractIfMissing(cfg, "git_branch", "$.ref")
+	setExtractIfMissing(cfg, "git_message", "$.head_commit.message")
+	setExtractIfMissing(cfg, "git_author", "$.head_commit.author.name")
+}
+
 func applyGitHubPRTemplate(cfg *config.WebhookConfig) {
 func applyGitHubPRTemplate(cfg *config.WebhookConfig) {
-	if cfg.AuthHeader == "" {
-		cfg.AuthHeader = "X-Hub-Signature-256"
-	}
-	if cfg.AuthType == "" {
-		cfg.AuthType = "hmac-sha256"
-	}
-	if len(cfg.MatchHeaders) == 0 {
-		cfg.MatchHeaders = make(map[string]string)
-	}
-	if _, exists := cfg.MatchHeaders["X-GitHub-Event"]; !exists {
-		cfg.MatchHeaders["X-GitHub-Event"] = "pull_request"
-	}
-	if len(cfg.Extract) == 0 {
-		cfg.Extract = make(map[string]string)
-	}
-	if _, exists := cfg.Extract["pr_number"]; !exists {
-		cfg.Extract["pr_number"] = "$.number"
-	}
-	if _, exists := cfg.Extract["pr_title"]; !exists {
-		cfg.Extract["pr_title"] = "$.pull_request.title"
-	}
-	if _, exists := cfg.Extract["pr_author"]; !exists {
-		cfg.Extract["pr_author"] = "$.pull_request.user.login"
-	}
-	if _, exists := cfg.Extract["pr_action"]; !exists {
-		cfg.Extract["pr_action"] = "$.action"
-	}
-	if _, exists := cfg.Extract["git_repository"]; !exists {
-		cfg.Extract["git_repository"] = "$.repository.full_name"
-	}
-	if _, exists := cfg.Extract["pr_state"]; !exists {
-		cfg.Extract["pr_state"] = "$.pull_request.state"
-	}
-	if _, exists := cfg.Extract["pr_head_sha"]; !exists {
-		cfg.Extract["pr_head_sha"] = "$.pull_request.head.sha"
-	}
+	setDefaultAuth(cfg)
+	ensureMatchHeaders(cfg)
+	setMatchHeaderIfMissing(cfg, "X-GitHub-Event", "pull_request")
+	ensureExtract(cfg)
+	setExtractIfMissing(cfg, "pr_number", "$.number")
+	setExtractIfMissing(cfg, "pr_title", "$.pull_request.title")
+	setExtractIfMissing(cfg, "pr_author", "$.pull_request.user.login")
+	setExtractIfMissing(cfg, "pr_action", "$.action")
+	setExtractIfMissing(cfg, "git_repository", "$.repository.full_name")
+	setExtractIfMissing(cfg, "pr_state", "$.pull_request.state")
+	setExtractIfMissing(cfg, "pr_head_sha", "$.pull_request.head.sha")
 }
 }
 
 
 func applyGitHubReleaseTemplate(cfg *config.WebhookConfig) {
 func applyGitHubReleaseTemplate(cfg *config.WebhookConfig) {
-	if cfg.AuthHeader == "" {
-		cfg.AuthHeader = "X-Hub-Signature-256"
-	}
-	if cfg.AuthType == "" {
-		cfg.AuthType = "hmac-sha256"
-	}
-	if len(cfg.MatchHeaders) == 0 {
-		cfg.MatchHeaders = make(map[string]string)
-	}
-	if _, exists := cfg.MatchHeaders["X-GitHub-Event"]; !exists {
-		cfg.MatchHeaders["X-GitHub-Event"] = "release"
-	}
-	if len(cfg.Extract) == 0 {
-		cfg.Extract = make(map[string]string)
-	}
-	if _, exists := cfg.Extract["release_action"]; !exists {
-		cfg.Extract["release_action"] = "$.action"
-	}
-	if _, exists := cfg.Extract["release_tag"]; !exists {
-		cfg.Extract["release_tag"] = "$.release.tag_name"
-	}
-	if _, exists := cfg.Extract["release_name"]; !exists {
-		cfg.Extract["release_name"] = "$.release.name"
-	}
-	if _, exists := cfg.Extract["git_repository"]; !exists {
-		cfg.Extract["git_repository"] = "$.repository.full_name"
-	}
-	if _, exists := cfg.Extract["release_author"]; !exists {
-		cfg.Extract["release_author"] = "$.release.author.login"
-	}
+	setDefaultAuth(cfg)
+	ensureMatchHeaders(cfg)
+	setMatchHeaderIfMissing(cfg, "X-GitHub-Event", "release")
+	ensureExtract(cfg)
+	setExtractIfMissing(cfg, "release_action", "$.action")
+	setExtractIfMissing(cfg, "release_tag", "$.release.tag_name")
+	setExtractIfMissing(cfg, "release_name", "$.release.name")
+	setExtractIfMissing(cfg, "git_repository", "$.repository.full_name")
+	setExtractIfMissing(cfg, "release_author", "$.release.author.login")
 }
 }
 
 
 func applyGitHubWorkflowTemplate(cfg *config.WebhookConfig) {
 func applyGitHubWorkflowTemplate(cfg *config.WebhookConfig) {
-	if cfg.AuthHeader == "" {
-		cfg.AuthHeader = "X-Hub-Signature-256"
-	}
-	if cfg.AuthType == "" {
-		cfg.AuthType = "hmac-sha256"
-	}
-	if len(cfg.MatchHeaders) == 0 {
-		cfg.MatchHeaders = make(map[string]string)
-	}
-	if _, exists := cfg.MatchHeaders["X-GitHub-Event"]; !exists {
-		cfg.MatchHeaders["X-GitHub-Event"] = "workflow_run"
-	}
-	if len(cfg.Extract) == 0 {
-		cfg.Extract = make(map[string]string)
-	}
-	if _, exists := cfg.Extract["workflow_name"]; !exists {
-		cfg.Extract["workflow_name"] = "$.workflow_run.name"
-	}
-	if _, exists := cfg.Extract["workflow_status"]; !exists {
-		cfg.Extract["workflow_status"] = "$.workflow_run.status"
-	}
-	if _, exists := cfg.Extract["workflow_conclusion"]; !exists {
-		cfg.Extract["workflow_conclusion"] = "$.workflow_run.conclusion"
-	}
-	if _, exists := cfg.Extract["git_repository"]; !exists {
-		cfg.Extract["git_repository"] = "$.repository.full_name"
-	}
-	if _, exists := cfg.Extract["git_commit"]; !exists {
-		cfg.Extract["git_commit"] = "$.workflow_run.head_sha"
-	}
-	if _, exists := cfg.Extract["git_branch"]; !exists {
-		cfg.Extract["git_branch"] = "$.workflow_run.head_branch"
-	}
+	setDefaultAuth(cfg)
+	ensureMatchHeaders(cfg)
+	setMatchHeaderIfMissing(cfg, "X-GitHub-Event", "workflow_run")
+	ensureExtract(cfg)
+	setExtractIfMissing(cfg, "workflow_name", "$.workflow_run.name")
+	setExtractIfMissing(cfg, "workflow_status", "$.workflow_run.status")
+	setExtractIfMissing(cfg, "workflow_conclusion", "$.workflow_run.conclusion")
+	setExtractIfMissing(cfg, "git_repository", "$.repository.full_name")
+	setExtractIfMissing(cfg, "git_commit", "$.workflow_run.head_sha")
+	setExtractIfMissing(cfg, "git_branch", "$.workflow_run.head_branch")
 }
 }

+ 52 - 43
service/internal/webhooks/handler.go

@@ -1,7 +1,6 @@
 package webhooks
 package webhooks
 
 
 import (
 import (
-	"encoding/json"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
 
 
@@ -34,70 +33,83 @@ func (h *WebhookHandler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	maxSize := int64(1024 * 1024)
-	payload, err := io.ReadAll(io.LimitReader(r.Body, maxSize))
+	payload, err := h.readPayload(r)
 	if err != nil {
 	if err != nil {
-		log.WithFields(log.Fields{
-			"error": err,
-		}).Warnf("Failed to read webhook payload")
 		http.Error(w, "Failed to read payload", http.StatusBadRequest)
 		http.Error(w, "Failed to read payload", http.StatusBadRequest)
 		return
 		return
 	}
 	}
 
 
-	var bodyData interface{}
-	if err := json.Unmarshal(payload, &bodyData); err != nil {
+	matchingActions := h.findMatchingActions(r, payload)
+	if len(matchingActions) == 0 {
+		h.writeOKResponse(w, "no matching webhook actions")
+		return
+	}
+
+	processed := h.processMatchingActions(matchingActions, r, payload)
+	log.WithFields(log.Fields{
+		"matched":   len(matchingActions),
+		"processed": processed,
+	}).Infof("Webhook processed")
+
+	h.writeOKResponse(w, "webhook actions")
+}
+
+func (h *WebhookHandler) readPayload(r *http.Request) ([]byte, error) {
+	maxSize := int64(1024 * 1024)
+	payload, err := io.ReadAll(io.LimitReader(r.Body, maxSize))
+	if err != nil {
 		log.WithFields(log.Fields{
 		log.WithFields(log.Fields{
 			"error": err,
 			"error": err,
-		}).Debugf("Webhook payload is not valid JSON")
+		}).Warnf("Failed to read webhook payload")
+		return nil, err
 	}
 	}
 
 
-	matchingActions := h.findMatchingActions(r, payload, bodyData)
+	return payload, nil
+}
 
 
-	if len(matchingActions) == 0 {
-		log.WithFields(log.Fields{
-			"path":   r.URL.Path,
-			"method": r.Method,
-		}).Debugf("No matching webhook actions found")
-		w.WriteHeader(http.StatusOK)
-		w.Write([]byte("OK"))
-		return
+func (h *WebhookHandler) writeOKResponse(w http.ResponseWriter, context string) {
+	w.WriteHeader(http.StatusOK)
+	if _, err := w.Write([]byte("OK")); err != nil {
+		log.WithError(err).Warnf("Failed to write response for %s", context)
 	}
 	}
+}
 
 
+func (h *WebhookHandler) processMatchingActions(matchingActions []ActionWebhookConfig, r *http.Request, payload []byte) int {
 	processed := 0
 	processed := 0
 	for _, actionConfig := range matchingActions {
 	for _, actionConfig := range matchingActions {
 		if h.processWebhook(actionConfig, r, payload) {
 		if h.processWebhook(actionConfig, r, payload) {
 			processed++
 			processed++
 		}
 		}
 	}
 	}
-
-	log.WithFields(log.Fields{
-		"matched":   len(matchingActions),
-		"processed": processed,
-	}).Infof("Webhook processed")
-
-	w.WriteHeader(http.StatusOK)
-	w.Write([]byte("OK"))
+	return processed
 }
 }
 
 
-func (h *WebhookHandler) findMatchingActions(r *http.Request, payload []byte, bodyData interface{}) []ActionWebhookConfig {
+func (h *WebhookHandler) findMatchingActions(r *http.Request, payload []byte) []ActionWebhookConfig {
 	var matches []ActionWebhookConfig
 	var matches []ActionWebhookConfig
 
 
 	for _, action := range h.cfg.Actions {
 	for _, action := range h.cfg.Actions {
-		for _, webhookConfig := range action.ExecOnWebhook {
-			webhookConfigCopy := webhookConfig
+		matches = append(matches, h.findMatchingWebhooksForAction(action, r, payload)...)
+	}
+
+	return matches
+}
 
 
-			if webhookConfigCopy.Template != "" {
-				ApplyGitHubTemplate(&webhookConfigCopy, webhookConfigCopy.Template)
-			}
+func (h *WebhookHandler) findMatchingWebhooksForAction(action *config.Action, r *http.Request, payload []byte) []ActionWebhookConfig {
+	var matches []ActionWebhookConfig
 
 
-			matcher := NewWebhookMatcher(webhookConfigCopy, r, payload, bodyData)
+	for _, webhookConfig := range action.ExecOnWebhook {
+		webhookConfigCopy := webhookConfig
 
 
-			if matcher.Matches() {
-				matches = append(matches, ActionWebhookConfig{
-					Action: action,
-					Config: webhookConfigCopy,
-				})
-			}
+		if webhookConfigCopy.Template != "" {
+			ApplyGitHubTemplate(&webhookConfigCopy, webhookConfigCopy.Template)
+		}
+
+		matcher := NewWebhookMatcher(webhookConfigCopy, r, payload)
+		if matcher.Matches() {
+			matches = append(matches, ActionWebhookConfig{
+				Action: action,
+				Config: webhookConfigCopy,
+			})
 		}
 		}
 	}
 	}
 
 
@@ -114,10 +126,7 @@ func (h *WebhookHandler) processWebhook(actionConfig ActionWebhookConfig, r *htt
 		return false
 		return false
 	}
 	}
 
 
-	var bodyData interface{}
-	json.Unmarshal(payload, &bodyData)
-
-	matcher := NewWebhookMatcher(actionConfig.Config, r, payload, bodyData)
+	matcher := NewWebhookMatcher(actionConfig.Config, r, payload)
 
 
 	args, err := matcher.ExtractArguments()
 	args, err := matcher.ExtractArguments()
 	if err != nil {
 	if err != nil {

+ 15 - 2
service/internal/webhooks/jsonpath.go

@@ -25,7 +25,13 @@ func (m *JSONMatcher) MatchPath(pathExpr string, expectedValue string) (bool, er
 		return false, err
 		return false, err
 	}
 	}
 
 
-	valueStr := fmt.Sprintf("%v", value)
+	// Marshal to JSON for consistent string representation
+	jsonBytes, err := json.Marshal(value)
+	if err != nil {
+		return false, fmt.Errorf("failed to marshal extracted value: %w", err)
+	}
+
+	valueStr := string(jsonBytes)
 	return valueStr == expectedValue, nil
 	return valueStr == expectedValue, nil
 }
 }
 
 
@@ -34,7 +40,14 @@ func (m *JSONMatcher) ExtractValue(pathExpr string) (string, error) {
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
-	return fmt.Sprintf("%v", value), nil
+
+	// Marshal to JSON for consistent string representation
+	jsonBytes, err := json.Marshal(value)
+	if err != nil {
+		return "", fmt.Errorf("failed to marshal extracted value: %w", err)
+	}
+
+	return string(jsonBytes), nil
 }
 }
 
 
 func (m *JSONMatcher) GetPayload() interface{} {
 func (m *JSONMatcher) GetPayload() interface{} {

+ 32 - 12
service/internal/webhooks/matcher.go

@@ -15,7 +15,7 @@ type WebhookMatcher struct {
 	bodyBytes []byte
 	bodyBytes []byte
 }
 }
 
 
-func NewWebhookMatcher(cfg config.WebhookConfig, r *http.Request, bodyBytes []byte, body interface{}) *WebhookMatcher {
+func NewWebhookMatcher(cfg config.WebhookConfig, r *http.Request, bodyBytes []byte) *WebhookMatcher {
 	return &WebhookMatcher{
 	return &WebhookMatcher{
 		config:    cfg,
 		config:    cfg,
 		req:       r,
 		req:       r,
@@ -83,13 +83,7 @@ func (m *WebhookMatcher) matchPath() bool {
 		return true
 		return true
 	}
 	}
 
 
-	parts := strings.SplitN(m.config.MatchPath, "=", 2)
-	jsonPath := parts[0]
-	expectedValue := ""
-	if len(parts) == 2 {
-		expectedValue = parts[1]
-	}
-
+	jsonPath, expectedValue := m.parseMatchPath()
 	matcher, err := NewJSONMatcher(m.bodyBytes)
 	matcher, err := NewJSONMatcher(m.bodyBytes)
 	if err != nil {
 	if err != nil {
 		log.WithFields(log.Fields{
 		log.WithFields(log.Fields{
@@ -98,6 +92,20 @@ func (m *WebhookMatcher) matchPath() bool {
 		return false
 		return false
 	}
 	}
 
 
+	return m.matchPathValue(matcher, jsonPath, expectedValue)
+}
+
+func (m *WebhookMatcher) parseMatchPath() (string, string) {
+	parts := strings.SplitN(m.config.MatchPath, "=", 2)
+	jsonPath := parts[0]
+	expectedValue := ""
+	if len(parts) == 2 {
+		expectedValue = parts[1]
+	}
+	return jsonPath, expectedValue
+}
+
+func (m *WebhookMatcher) matchPathValue(matcher *JSONMatcher, jsonPath, expectedValue string) bool {
 	if expectedValue == "" {
 	if expectedValue == "" {
 		_, err := matcher.ExtractValue(jsonPath)
 		_, err := matcher.ExtractValue(jsonPath)
 		return err == nil
 		return err == nil
@@ -131,13 +139,21 @@ func (m *WebhookMatcher) compareValues(actual, expected string) bool {
 }
 }
 
 
 func (m *WebhookMatcher) ExtractArguments() (map[string]string, error) {
 func (m *WebhookMatcher) ExtractArguments() (map[string]string, error) {
-	args := make(map[string]string)
-
 	matcher, err := NewJSONMatcher(m.bodyBytes)
 	matcher, err := NewJSONMatcher(m.bodyBytes)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	args := m.extractJSONPathValues(matcher)
+	m.addWebhookMetadata(args)
+	m.addWebhookHeaders(args)
+
+	return args, nil
+}
+
+func (m *WebhookMatcher) extractJSONPathValues(matcher *JSONMatcher) map[string]string {
+	args := make(map[string]string)
+
 	for argName, jsonPath := range m.config.Extract {
 	for argName, jsonPath := range m.config.Extract {
 		value, err := matcher.ExtractValue(jsonPath)
 		value, err := matcher.ExtractValue(jsonPath)
 		if err != nil {
 		if err != nil {
@@ -151,15 +167,19 @@ func (m *WebhookMatcher) ExtractArguments() (map[string]string, error) {
 		args[argName] = value
 		args[argName] = value
 	}
 	}
 
 
+	return args
+}
+
+func (m *WebhookMatcher) addWebhookMetadata(args map[string]string) {
 	args["webhook_method"] = m.req.Method
 	args["webhook_method"] = m.req.Method
 	args["webhook_path"] = m.req.URL.Path
 	args["webhook_path"] = m.req.URL.Path
 	args["webhook_query"] = m.req.URL.RawQuery
 	args["webhook_query"] = m.req.URL.RawQuery
+}
 
 
+func (m *WebhookMatcher) addWebhookHeaders(args map[string]string) {
 	for key, values := range m.req.Header {
 	for key, values := range m.req.Header {
 		if len(values) > 0 {
 		if len(values) > 0 {
 			args["webhook_header_"+strings.ToLower(key)] = values[0]
 			args["webhook_header_"+strings.ToLower(key)] = values[0]
 		}
 		}
 	}
 	}
-
-	return args, nil
 }
 }