From 56a5b0d63100975c38a3cf48758e0efbde4ae28f Mon Sep 17 00:00:00 2001 From: lionelresnik Date: Mon, 16 Mar 2026 12:01:21 +0200 Subject: [PATCH 1/2] feat(webhook): add Authorization header support for webhook outputs Add Headers field to WebhookOutput struct and modify Send() to inject custom headers into HTTP requests. This enables Bearer Token and Basic Authentication for webhook notifications. Resolves: SLK-110843 Made-with: Cursor --- outputs/webhook.go | 22 ++++++-- outputs/webhook_test.go | 111 ++++++++++++++++++++++++++++++++++++++++ router/builders.go | 5 +- 3 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 outputs/webhook_test.go diff --git a/outputs/webhook.go b/outputs/webhook.go index d70e1f8c..d3b30162 100644 --- a/outputs/webhook.go +++ b/outputs/webhook.go @@ -17,8 +17,9 @@ const ( ) type WebhookOutput struct { - Name string - Url string + Name string + Url string + Headers map[string][]string } func (webhook *WebhookOutput) GetType() string { @@ -47,7 +48,22 @@ func (webhook *WebhookOutput) Init() error { func (webhook *WebhookOutput) Send(content map[string]string) (data.OutputResponse, error) { log.Logger.Infof("Sending webhook to %q", webhook.Url) dataStr := content["description"] //it's not supposed to work with legacy renderer - resp, err := http.Post(webhook.Url, "application/json", strings.NewReader(dataStr)) + + req, err := http.NewRequest("POST", webhook.Url, strings.NewReader(dataStr)) + if err != nil { + log.Logger.Errorf("Sending webhook Error: %v", err) + return data.OutputResponse{}, err + } + req.Header.Set("Content-Type", "application/json") + + for key, values := range webhook.Headers { + for _, v := range values { + req.Header.Add(key, v) + } + } + + client := &http.Client{} + resp, err := client.Do(req) if err != nil { log.Logger.Errorf("Sending webhook Error: %v", err) return data.OutputResponse{}, err diff --git a/outputs/webhook_test.go b/outputs/webhook_test.go new file mode 100644 index 00000000..0131a10d --- /dev/null +++ b/outputs/webhook_test.go @@ -0,0 +1,111 @@ +package outputs + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWebhookOutput_Send_WithHeaders(t *testing.T) { + tests := []struct { + name string + headers map[string][]string + expectedHeaders map[string]string + }{ + { + name: "Bearer token auth", + headers: map[string][]string{ + "Authorization": {"Bearer test-token-123"}, + }, + expectedHeaders: map[string]string{ + "Authorization": "Bearer test-token-123", + "Content-Type": "application/json", + }, + }, + { + name: "Basic auth", + headers: map[string][]string{ + "Authorization": {"Basic dXNlcjpwYXNz"}, + }, + expectedHeaders: map[string]string{ + "Authorization": "Basic dXNlcjpwYXNz", + "Content-Type": "application/json", + }, + }, + { + name: "No headers", + headers: nil, + expectedHeaders: map[string]string{ + "Content-Type": "application/json", + }, + }, + { + name: "Multiple custom headers", + headers: map[string][]string{ + "Authorization": {"Bearer token"}, + "X-Custom": {"custom-value"}, + }, + expectedHeaders: map[string]string{ + "Authorization": "Bearer token", + "X-Custom": "custom-value", + "Content-Type": "application/json", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var capturedHeaders http.Header + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedHeaders = r.Header + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + webhook := &WebhookOutput{ + Name: "test-webhook", + Url: server.URL, + Headers: tt.headers, + } + + _, err := webhook.Send(map[string]string{"description": `{"test": "data"}`}) + assert.NoError(t, err) + + for key, expectedValue := range tt.expectedHeaders { + assert.Equal(t, expectedValue, capturedHeaders.Get(key), "Header %s mismatch", key) + } + }) + } +} + +func TestWebhookOutput_Send_ErrorCases(t *testing.T) { + t.Run("Bad URL", func(t *testing.T) { + webhook := &WebhookOutput{ + Name: "test-webhook", + Url: "not-a-valid-url://invalid", + } + + _, err := webhook.Send(map[string]string{"description": `{}`}) + assert.Error(t, err) + }) + + t.Run("Server returns error status", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Unauthorized")) + })) + defer server.Close() + + webhook := &WebhookOutput{ + Name: "test-webhook", + Url: server.URL, + } + + _, err := webhook.Send(map[string]string{"description": `{}`}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Unauthorized") + }) +} diff --git a/router/builders.go b/router/builders.go index dfa7df56..2d23949a 100644 --- a/router/builders.go +++ b/router/builders.go @@ -27,8 +27,9 @@ func buildSplunkOutput(sourceSettings *data.OutputSettings) *outputs.SplunkOutpu func buildWebhookOutput(sourceSettings *data.OutputSettings) *outputs.WebhookOutput { return &outputs.WebhookOutput{ - Name: sourceSettings.Name, - Url: sourceSettings.Url, + Name: sourceSettings.Name, + Url: sourceSettings.Url, + Headers: sourceSettings.Headers, } } From 991e4eae9ffecd4426f55d15a656eea9be1c590f Mon Sep 17 00:00:00 2001 From: lionelresnik Date: Mon, 16 Mar 2026 15:08:55 +0200 Subject: [PATCH 2/2] SLK-110843 fix(webhook): handle Write error return value in test Made-with: Cursor --- outputs/webhook_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/outputs/webhook_test.go b/outputs/webhook_test.go index 0131a10d..88fefcee 100644 --- a/outputs/webhook_test.go +++ b/outputs/webhook_test.go @@ -95,7 +95,7 @@ func TestWebhookOutput_Send_ErrorCases(t *testing.T) { t.Run("Server returns error status", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte("Unauthorized")) + _, _ = w.Write([]byte("Unauthorized")) })) defer server.Close()