From c5f646923e3d9ed5c00ab0c394e97a77dbcabf4f Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Mon, 17 Nov 2025 10:40:44 -0800 Subject: [PATCH 1/3] Fix URL encoding bug in Defender adapter OData filter query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manual percent-encoding of query parameters caused malformed URLs where the datetime value included query parameter syntax (e.g., '2025-11-17T10:27:18.440439Z?$filter=createdDateTime'). This resulted in Microsoft API BadRequest errors. Replace manual string concatenation with proper net/url package usage to ensure correct URL construction and parameter escaping. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- defender/client.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/defender/client.go b/defender/client.go index 13da601..694012b 100644 --- a/defender/client.go +++ b/defender/client.go @@ -9,7 +9,7 @@ import ( "io/ioutil" "net" "net/http" - "strings" + "net/url" "sync" "time" @@ -60,7 +60,7 @@ func (c *DefenderConfig) Validate() error { return nil } -func NewDefenderAdapter(conf DefenderConfig) (*DefenderAdapter, chan struct{}, error) { +func NewDefenderAdapter(ctx context.Context, conf DefenderConfig) (*DefenderAdapter, chan struct{}, error) { var err error a := &DefenderAdapter{ conf: conf, @@ -68,7 +68,7 @@ func NewDefenderAdapter(conf DefenderConfig) (*DefenderAdapter, chan struct{}, e doStop: utils.NewEvent(), } - a.uspClient, err = uspclient.NewClient(conf.ClientOptions) + a.uspClient, err = uspclient.NewClient(ctx, conf.ClientOptions) if err != nil { return nil, nil, err } @@ -195,13 +195,19 @@ func (a *DefenderAdapter) makeOneListRequest(eventsUrl string, since string, las // Retry up to 3 times for attempt := 1; attempt <= 3; attempt++ { - // Create query parameters - filter := "%24" - query := "%20ge%20" - date_filter := fmt.Sprintf("?%sfilter=createdDateTime%s%s", filter, query, strings.Replace(since, ":", "%3A", -1)) + // Parse the base URL and add query parameters properly + parsedURL, err := url.Parse(eventsUrl) + if err != nil { + a.conf.ClientOptions.OnError(fmt.Errorf("Error parsing URL: %s\n", err)) + return nil, since, "", err + } + + // Build the OData filter query parameter properly + // Use RawQuery to set $filter parameter ($ is valid in query strings) + filterValue := fmt.Sprintf("createdDateTime ge %s", since) + parsedURL.RawQuery = "$filter=" + url.QueryEscape(filterValue) - // Create the full request URL with query parameters (don't modify eventsUrl to avoid corruption on retries) - requestUrl := eventsUrl + date_filter + requestUrl := parsedURL.String() authToken, err := a.fetchToken() if err != nil { From 212c72bc2f6615c60b30a52225eb2a7493f8cf57 Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Mon, 17 Nov 2025 10:51:08 -0800 Subject: [PATCH 2/3] Update dep --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 9f8fbba..9a6da46 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/nxadm/tail v1.4.8 github.com/refractionPOINT/evtx v0.0.0-20250821225651-06f8e57ee121 github.com/refractionPOINT/gjson v0.0.0-20230509223721-3a6dd216c22d - github.com/refractionPOINT/go-limacharlie/limacharlie v0.0.0-20251110144611-385ee44c7cf9 + github.com/refractionPOINT/go-limacharlie/limacharlie v0.0.0-20251116170209-61e4b9651299 github.com/refractionPOINT/go-uspclient v1.6.0 github.com/stretchr/testify v1.11.1 github.com/vmihailenco/msgpack/v5 v5.4.1 @@ -74,7 +74,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.24.1 // indirect github.com/go-openapi/errors v0.22.4 // indirect - github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonpointer v0.22.3 // indirect github.com/go-openapi/jsonreference v0.21.3 // indirect github.com/go-openapi/loads v0.23.2 // indirect github.com/go-openapi/runtime v0.29.2 // indirect diff --git a/go.sum b/go.sum index d585894..9a31787 100644 --- a/go.sum +++ b/go.sum @@ -162,8 +162,8 @@ github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rB github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84= github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM= github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= -github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= -github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8= +github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= @@ -308,8 +308,8 @@ github.com/refractionPOINT/evtx v0.0.0-20250821225651-06f8e57ee121 h1:8VEMti13FI github.com/refractionPOINT/evtx v0.0.0-20250821225651-06f8e57ee121/go.mod h1:ZQQ/GaL5743AyXXzuQgC9rcZzfNA1qKXWQv/4iVdGMo= github.com/refractionPOINT/gjson v0.0.0-20230509223721-3a6dd216c22d h1:5OLPrBvVR0V/eGezlkJ4fyTaaJS/7pbYNTzMqu0Qur0= github.com/refractionPOINT/gjson v0.0.0-20230509223721-3a6dd216c22d/go.mod h1:7S4MA6zBzHBCOlHUFK+X+lgQq+pHiR0OZtW0v3VJOkY= -github.com/refractionPOINT/go-limacharlie/limacharlie v0.0.0-20251110144611-385ee44c7cf9 h1:X8aYEHo8N+Esf+o2dWLklfShx00TwWf80fUcj3It8zE= -github.com/refractionPOINT/go-limacharlie/limacharlie v0.0.0-20251110144611-385ee44c7cf9/go.mod h1:M4LyEs/2mJ2OA1JHsDMQYKPdD8/RZmz2+FWLlVIwapM= +github.com/refractionPOINT/go-limacharlie/limacharlie v0.0.0-20251116170209-61e4b9651299 h1:P6rwrNYePV46kMzZEV8LzplZ8I9hUHxIr0tYloj3U/4= +github.com/refractionPOINT/go-limacharlie/limacharlie v0.0.0-20251116170209-61e4b9651299/go.mod h1:M4LyEs/2mJ2OA1JHsDMQYKPdD8/RZmz2+FWLlVIwapM= github.com/refractionPOINT/go-uspclient v1.6.0 h1:obGaLRkJlcIvZHxf55JSXmT2pDLqnX3E6IPDUT572pY= github.com/refractionPOINT/go-uspclient v1.6.0/go.mod h1:j4tCnHNLMcx80nZ2KHKLGgLFPW2UVvFVxRil6Qd6Wjo= github.com/refractionPOINT/tail v0.0.0-20211216163028-4472660a31a6 h1:1+7g49ZLqdbxFcqy0ISBs6tXq5dl/D0WV/VQgcvqWhY= From d90a1004a6325afaa07217b8c31e9d26b53c629c Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Mon, 24 Nov 2025 15:29:24 -0800 Subject: [PATCH 3/3] Adding debug logs --- defender/client.go | 57 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/defender/client.go b/defender/client.go index 694012b..0680e49 100644 --- a/defender/client.go +++ b/defender/client.go @@ -84,7 +84,7 @@ func NewDefenderAdapter(ctx context.Context, conf DefenderConfig) (*DefenderAdap a.chStopped = make(chan struct{}) - a.conf.ClientOptions.DebugLog(fmt.Sprintf("starting to fetch alerts")) + a.conf.ClientOptions.DebugLog("starting to fetch alerts") a.wgSenders.Add(1) go a.fetchEvents(URL["get_alerts"]) @@ -117,35 +117,64 @@ func (a *DefenderAdapter) fetchToken() (string, error) { url := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", a.conf.TenantID) payload := fmt.Sprintf("client_id=%s&scope=%s&grant_type=%s&client_secret=%s", a.conf.ClientID, scope, "client_credentials", a.conf.ClientSecret) + // Log the request details (mask sensitive data) + maskedPayload := fmt.Sprintf("client_id=%s&scope=%s&grant_type=%s&client_secret=***REDACTED***", a.conf.ClientID, scope, "client_credentials") + a.conf.ClientOptions.DebugLog(fmt.Sprintf("fetchToken: POST %s", url)) + a.conf.ClientOptions.DebugLog(fmt.Sprintf("fetchToken: Request payload: %s", maskedPayload)) + req, err := http.NewRequest("POST", url, bytes.NewBufferString(payload)) if err != nil { return "", fmt.Errorf("no bearer token returned: %s", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + a.conf.ClientOptions.DebugLog(fmt.Sprintf("fetchToken: Request headers: Content-Type=application/x-www-form-urlencoded")) client := &http.Client{} resp, err := client.Do(req) if err != nil { + a.conf.ClientOptions.DebugLog(fmt.Sprintf("fetchToken: Request failed: %s", err)) return "", fmt.Errorf("no bearer token returned: %s", err) } defer resp.Body.Close() + a.conf.ClientOptions.DebugLog(fmt.Sprintf("fetchToken: Response status: %d %s", resp.StatusCode, resp.Status)) + body, err := ioutil.ReadAll(resp.Body) if err != nil { + a.conf.ClientOptions.DebugLog(fmt.Sprintf("fetchToken: Failed to read response body: %s", err)) return "", fmt.Errorf("no bearer token returned: %s", err) } + // Log response body (mask access token if present) var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { + a.conf.ClientOptions.DebugLog(fmt.Sprintf("fetchToken: Response body (invalid JSON): %s", string(body))) return "", fmt.Errorf("no bearer token returned: %s", err) } + // Create a copy for logging with masked token + logResult := make(map[string]interface{}) + for k, v := range result { + if k == "access_token" { + if token, ok := v.(string); ok && len(token) > 20 { + logResult[k] = token[:10] + "..." + token[len(token)-10:] + " (masked)" + } else { + logResult[k] = "***REDACTED***" + } + } else { + logResult[k] = v + } + } + logJSON, _ := json.Marshal(logResult) + a.conf.ClientOptions.DebugLog(fmt.Sprintf("fetchToken: Response body: %s", string(logJSON))) + accessToken, ok := result["access_token"].(string) if !ok { return "", fmt.Errorf("no bearer token returned: %#v", result) } + a.conf.ClientOptions.DebugLog("fetchToken: successfully obtained access token") return accessToken, nil } @@ -209,6 +238,8 @@ func (a *DefenderAdapter) makeOneListRequest(eventsUrl string, since string, las requestUrl := parsedURL.String() + a.conf.ClientOptions.DebugLog(fmt.Sprintf("makeOneListRequest: Attempt %d of 3", attempt)) + authToken, err := a.fetchToken() if err != nil { // Retry if token fetch failed, but continue to the next iteration @@ -222,6 +253,8 @@ func (a *DefenderAdapter) makeOneListRequest(eventsUrl string, since string, las return nil, since, "", fmt.Errorf("error fetching token after 3 attempts: %s", err) } + a.conf.ClientOptions.DebugLog(fmt.Sprintf("makeOneListRequest: GET %s", requestUrl)) + req, err := http.NewRequest("GET", requestUrl, nil) if err != nil { a.conf.ClientOptions.OnError(fmt.Errorf("Error creating request: %s\n", err)) @@ -231,20 +264,41 @@ func (a *DefenderAdapter) makeOneListRequest(eventsUrl string, since string, las req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authToken)) req.Header.Set("Content-Type", "application/json") + // Log request headers (mask the actual token) + var maskedToken string + if len(authToken) > 20 { + maskedToken = authToken[:10] + "..." + authToken[len(authToken)-10:] + } else { + maskedToken = "***REDACTED***" + } + a.conf.ClientOptions.DebugLog(fmt.Sprintf("makeOneListRequest: Request headers: Authorization=Bearer %s, Content-Type=application/json", maskedToken)) + client := &http.Client{} resp, err := client.Do(req) if err != nil { + a.conf.ClientOptions.DebugLog(fmt.Sprintf("makeOneListRequest: Request failed: %s", err)) a.conf.ClientOptions.OnError(fmt.Errorf("Error making request: %s\n", err)) return nil, since, "", err } defer resp.Body.Close() + a.conf.ClientOptions.DebugLog(fmt.Sprintf("makeOneListRequest: Response status: %d %s", resp.StatusCode, resp.Status)) + body, err := ioutil.ReadAll(resp.Body) if err != nil { + a.conf.ClientOptions.DebugLog(fmt.Sprintf("makeOneListRequest: Failed to read response body: %s", err)) a.conf.ClientOptions.OnError(fmt.Errorf("Error reading response: %s\n", err)) return nil, since, "", err } + // Log response body length and first 500 chars (if it's too long) + bodyStr := string(body) + if len(bodyStr) > 500 { + a.conf.ClientOptions.DebugLog(fmt.Sprintf("makeOneListRequest: Response body (%d bytes): %s...[truncated]", len(bodyStr), bodyStr[:500])) + } else { + a.conf.ClientOptions.DebugLog(fmt.Sprintf("makeOneListRequest: Response body (%d bytes): %s", len(bodyStr), bodyStr)) + } + if resp.StatusCode != http.StatusOK { // Check for retryable status codes (503, 504) - likely Microsoft infrastructure issues isRetryable := resp.StatusCode == http.StatusServiceUnavailable || resp.StatusCode == http.StatusGatewayTimeout @@ -276,6 +330,7 @@ func (a *DefenderAdapter) makeOneListRequest(eventsUrl string, since string, las } items := detections + a.conf.ClientOptions.DebugLog(fmt.Sprintf("makeOneListRequest: Successfully parsed response, found %d alerts", len(items))) lastDetectionTime = since for _, detection := range items {