From ddb6b827ff7bf6f36fed8abb82bacd2dacc3519e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Petrov?= Date: Tue, 31 Mar 2026 22:40:42 +0200 Subject: [PATCH 1/5] GlobMap is map[glob.Pattern]glob.Pattern --- .../telemetry/centralclient/interceptors.go | 12 +-- .../centralclient/interceptors_test.go | 4 + pkg/telemetry/phonehome/campaign.go | 13 ++- pkg/telemetry/phonehome/campaign_test.go | 91 +++++++++++++++++++ pkg/telemetry/phonehome/headers_multimap.go | 21 +++++ .../phonehome/headers_multimap_test.go | 32 +++++++ pkg/telemetry/phonehome/interceptor.go | 3 +- pkg/telemetry/phonehome/interceptor_test.go | 4 + pkg/telemetry/phonehome/request_params.go | 12 ++- .../phonehome/request_params_test.go | 5 +- 10 files changed, 175 insertions(+), 22 deletions(-) diff --git a/central/telemetry/centralclient/interceptors.go b/central/telemetry/centralclient/interceptors.go index 614b28276b4cb..cf82bc59c408c 100644 --- a/central/telemetry/centralclient/interceptors.go +++ b/central/telemetry/centralclient/interceptors.go @@ -8,13 +8,7 @@ import ( "github.com/stackrox/rox/pkg/telemetry/phonehome" ) -const ( - // The header is set by the RHACS ServiceNow integration. - // See https://github.com/stackrox/service-now/blob/9d1df943f5f0b3052df97c6272814e2303f17685/52616ff6938a1a50c52a72856aba10fd/update/sys_script_include_2b362bbe938a1a50c52a72856aba10b3.xml#L80. - snowIntegrationHeader = "Rh-Servicenow-Integration" - - userAgentHeaderKey = "User-Agent" -) +const userAgentHeaderKey = "User-Agent" var ( ignoredPaths = glob.Pattern("{/v1/ping,/v1.PingService/Ping,/v1/metadata,/static/*}") @@ -34,8 +28,8 @@ var ( // ServiceNow default User-Agent includes "ServiceNow", but // customers are free to change it. // See https://support.servicenow.com/kb?id=kb_article_view&sysparm_article=KB1511513. - userAgentHeaderKey: "*ServiceNow*", - snowIntegrationHeader: phonehome.NoHeaderOrAnyValue, + userAgentHeaderKey: "*ServiceNow*", + "Rh-*": phonehome.NoHeaderOrAnyValue, }, }, // Capture requests from GitHub action user agents. diff --git a/central/telemetry/centralclient/interceptors_test.go b/central/telemetry/centralclient/interceptors_test.go index 2262167bca6ef..8c1b61057a276 100644 --- a/central/telemetry/centralclient/interceptors_test.go +++ b/central/telemetry/centralclient/interceptors_test.go @@ -8,6 +8,10 @@ import ( "github.com/stretchr/testify/require" ) +// The header is set by the RHACS ServiceNow integration. +// See https://github.com/stackrox/service-now/blob/9d1df943f5f0b3052df97c6272814e2303f17685/52616ff6938a1a50c52a72856aba10fd/update/sys_script_include_2b362bbe938a1a50c52a72856aba10b3.xml#L80. +const snowIntegrationHeader = "Rh-Servicenow-Integration" + func withUserAgent(ua string) phonehome.Headers { return phonehome.Headers{userAgentHeaderKey: {ua}} } diff --git a/pkg/telemetry/phonehome/campaign.go b/pkg/telemetry/phonehome/campaign.go index de19c8510e25b..6cc4cd96504fb 100644 --- a/pkg/telemetry/phonehome/campaign.go +++ b/pkg/telemetry/phonehome/campaign.go @@ -27,9 +27,12 @@ func (c *APICallCampaignCriterion) Compile() error { if c == nil { return nil } - for _, pattern := range c.Headers { - if err := pattern.Compile(); err != nil { - return errors.WithMessage(err, "error parsing header pattern") + for name, value := range c.Headers { + if err := name.Compile(); err != nil { + return errors.WithMessage(err, "error parsing header name pattern") + } + if err := value.Compile(); err != nil { + return errors.WithMessage(err, "error parsing header value pattern") } } if err := c.Path.Compile(); err != nil { @@ -89,10 +92,10 @@ func PathPattern(pattern glob.Pattern) *APICallCampaignCriterion { } // HeaderPattern builds a header pattern criterion. -func HeaderPattern(header string, pattern glob.Pattern) *APICallCampaignCriterion { +func HeaderPattern(header glob.Pattern, value glob.Pattern) *APICallCampaignCriterion { return &APICallCampaignCriterion{ Headers: GlobMap{ - header: pattern, + glob.Pattern(header): glob.Pattern(value), }, } } diff --git a/pkg/telemetry/phonehome/campaign_test.go b/pkg/telemetry/phonehome/campaign_test.go index 32a25910d85fc..1dbcf39bf9265 100644 --- a/pkg/telemetry/phonehome/campaign_test.go +++ b/pkg/telemetry/phonehome/campaign_test.go @@ -190,6 +190,97 @@ func TestCampaignFulfilled(t *testing.T) { assert.Same(t, campaign[1], fulfilled[1]) } }) + t.Run("Header name and value globs", func(t *testing.T) { + headers := Headers{ + "X-Custom-One": {"alpha"}, + "X-Custom-Two": {"beta"}, + "X-Other": {"gamma"}, + } + rp := &RequestParams{ + Headers: headers, + Method: "GET", + Path: "/test", + Code: 200, + } + + t.Run("Glob header name matches", func(t *testing.T) { + campaign := APICallCampaign{ + HeaderPattern("X-Custom-*", "*"), + } + require.NoError(t, campaign.Compile()) + assert.Equal(t, 1, campaign.CountFulfilled(rp, doNothing)) + }) + + t.Run("Glob header name and value match", func(t *testing.T) { + campaign := APICallCampaign{ + HeaderPattern("X-Custom-*", "al*"), + } + require.NoError(t, campaign.Compile()) + assert.Equal(t, 1, campaign.CountFulfilled(rp, doNothing)) + }) + + t.Run("Glob header name matches but value does not", func(t *testing.T) { + campaign := APICallCampaign{ + HeaderPattern("X-Custom-*", "zzz*"), + } + require.NoError(t, campaign.Compile()) + assert.Zero(t, campaign.CountFulfilled(rp, doNothing)) + }) + + t.Run("Glob header name does not match", func(t *testing.T) { + campaign := APICallCampaign{ + HeaderPattern("X-Missing-*", "*"), + } + require.NoError(t, campaign.Compile()) + assert.Zero(t, campaign.CountFulfilled(rp, doNothing)) + }) + + t.Run("Multiple glob header criteria", func(t *testing.T) { + campaign := APICallCampaign{ + { + Headers: GlobMap{ + "X-Custom-*": "al*", + "X-Other": "gam*", + }, + }, + } + require.NoError(t, campaign.Compile()) + assert.Equal(t, 1, campaign.CountFulfilled(rp, doNothing)) + }) + + t.Run("Method match captures all X- headers", func(t *testing.T) { + campaign := APICallCampaign{ + { + Method: glob.Pattern("GET").Ptr(), + Headers: GlobMap{"X-*": "*"}, + }, + } + require.NoError(t, campaign.Compile()) + assert.Equal(t, 1, campaign.CountFulfilled(rp, doNothing)) + + rpPost := &RequestParams{ + Headers: headers, + Method: "POST", + Path: "/test", + Code: 200, + } + assert.Zero(t, campaign.CountFulfilled(rpPost, doNothing)) + }) + + t.Run("Capture by method with missing glob header pattern", func(t *testing.T) { + campaign := APICallCampaign{ + { + Method: glob.Pattern("GET").Ptr(), + Headers: GlobMap{"X-*": NoHeaderOrAnyValue}, + }, + } + require.NoError(t, campaign.Compile()) + assert.Equal(t, 1, campaign.CountFulfilled(rp, doNothing)) + + campaign[0].Method = glob.Pattern("POST").Ptr() + assert.Zero(t, campaign.CountFulfilled(rp, doNothing)) + }) + }) t.Run("All criteria", func(t *testing.T) { campaign := APICallCampaign{ diff --git a/pkg/telemetry/phonehome/headers_multimap.go b/pkg/telemetry/phonehome/headers_multimap.go index 3e411cf4cfdb6..17ca1800f772e 100644 --- a/pkg/telemetry/phonehome/headers_multimap.go +++ b/pkg/telemetry/phonehome/headers_multimap.go @@ -51,6 +51,27 @@ func (h Headers) GetMatching(key string, value glob.Pattern) []string { return result } +// GetAll returns filtered map of the headers and their values. +func (h Headers) GetAll(canonicalKey glob.Pattern, value glob.Pattern) (map[string][]string, error) { + if err := canonicalKey.Compile(); err != nil { + return nil, err + } + if err := value.Compile(); err != nil { + return nil, err + } + result := make(map[string][]string) + for key, values := range h { + if canonicalKey.Match(key) { + for _, v := range values { + if value == NoHeaderOrAnyValue || value.Match(v) { + result[key] = append(result[key], v) + } + } + } + } + return result, nil +} + // Set implements the setter interface. func (h Headers) Set(key string, values ...string) { for i, value := range values { diff --git a/pkg/telemetry/phonehome/headers_multimap_test.go b/pkg/telemetry/phonehome/headers_multimap_test.go index fff7567698783..39c67272efdae 100644 --- a/pkg/telemetry/phonehome/headers_multimap_test.go +++ b/pkg/telemetry/phonehome/headers_multimap_test.go @@ -121,3 +121,35 @@ func TestGetMatching(t *testing.T) { }) } } + +func TestGetAll(t *testing.T) { + h := make(http.Header) + h.Add("key-1", "value 1") + h.Add("key-2", "value 2") + h.Add("key-2", "value 1") + h.Add("something-else", "value 2") + h.Add("something-else", "value 3") + + headers := Headers(h) + matching, err := headers.GetAll("Key-*", "value 1") + assert.NoError(t, err) + assert.Equal(t, map[string][]string{"Key-1": {"value 1"}, "Key-2": {"value 1"}}, matching) + + matching, err = headers.GetAll("nope", "value 1") + assert.NoError(t, err) + assert.Empty(t, matching) + + matching, err = headers.GetAll("Key-1", "nope") + assert.NoError(t, err) + assert.Empty(t, matching) + + _, err = headers.GetAll("Key-[1-]", "nope") + assert.Error(t, err) + + _, err = headers.GetAll("Key-1", "value [1-]") + assert.Error(t, err) + + matching, err = headers.GetAll("*", "value [2-3]") + assert.NoError(t, err) + assert.Equal(t, map[string][]string{"Something-Else": {"value 2", "value 3"}, "Key-2": {"value 2"}}, matching) +} diff --git a/pkg/telemetry/phonehome/interceptor.go b/pkg/telemetry/phonehome/interceptor.go index 68ab6448a7b38..ed0e135c1e83a 100644 --- a/pkg/telemetry/phonehome/interceptor.go +++ b/pkg/telemetry/phonehome/interceptor.go @@ -60,8 +60,7 @@ func getGRPCRequestDetails(ctx context.Context, err error, grpcFullMethod string if ri.HTTPRequest.URL != nil { path = ri.HTTPRequest.URL.Path } - // Override the User-Agent with the gRPC client or the grpc-gateway user - // agent. + // Override the User-Agent with the gRPC client or the grpc-gateway user agent. grpcClientAgent := ri.Metadata.Get(userAgentHeaderKey) if clientAgent := ri.HTTPRequest.Headers.Get(userAgentHeaderKey); clientAgent != "" { grpcClientAgent = append(grpcClientAgent, clientAgent) diff --git a/pkg/telemetry/phonehome/interceptor_test.go b/pkg/telemetry/phonehome/interceptor_test.go index c28ee7f53ffe4..b25b70c2937f7 100644 --- a/pkg/telemetry/phonehome/interceptor_test.go +++ b/pkg/telemetry/phonehome/interceptor_test.go @@ -163,6 +163,10 @@ func (s *interceptorTestSuite) TestGrpcRequestInfo() { ua := rp.Headers.Get("User-Agent") s.NoError(err) s.Equal([]string{"test"}, ua) + + matching, err := rp.Headers.GetAll("User-*", "*") + s.NoError(err) + s.Equal(map[string][]string{"User-Agent": {"test"}}, matching) } func (s *interceptorTestSuite) TestGrpcWithHTTPRequestInfo() { diff --git a/pkg/telemetry/phonehome/request_params.go b/pkg/telemetry/phonehome/request_params.go index 8bc1d624e1795..86ddf1d37da0e 100644 --- a/pkg/telemetry/phonehome/request_params.go +++ b/pkg/telemetry/phonehome/request_params.go @@ -2,6 +2,7 @@ package phonehome import ( "context" + "maps" "net/http" "github.com/stackrox/rox/pkg/glob" @@ -23,7 +24,7 @@ type RequestParams struct { Headers Headers } -type GlobMap map[string]glob.Pattern +type GlobMap map[glob.Pattern]glob.Pattern // MatchHeaders checks whether the request headers satisfy all given patterns. // Returns nil if any pattern fails to match or if headers are absent. Returns @@ -33,14 +34,17 @@ type GlobMap map[string]glob.Pattern func (rp *RequestParams) MatchHeaders(patterns GlobMap) Headers { result := make(Headers) for header, expression := range patterns { - values := rp.Headers.GetMatching(header, expression) - if values == nil { + matching, err := rp.Headers.GetAll(header, expression) + if err != nil { + return nil + } + if matching == nil { if expression != NoHeaderOrAnyValue { return nil } continue } - result[header] = values + maps.Copy(result, matching) } return result } diff --git a/pkg/telemetry/phonehome/request_params_test.go b/pkg/telemetry/phonehome/request_params_test.go index 4eb2b08437dc1..decc0fc5849b4 100644 --- a/pkg/telemetry/phonehome/request_params_test.go +++ b/pkg/telemetry/phonehome/request_params_test.go @@ -1,6 +1,7 @@ package phonehome import ( + "net/http" "testing" "github.com/stretchr/testify/assert" @@ -17,11 +18,11 @@ func TestMatchHeaders(t *testing.T) { }) rp := RequestParams{ - Headers: Headers{ + Headers: Headers(http.Header{ "Empty": {}, "One": {"one"}, "Two": {"one", "two"}, - }, + }), } tests := map[string]struct { From 17724c118c41d19b6629d9fd2f95f50a0b9e4f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Petrov?= Date: Wed, 1 Apr 2026 18:09:53 +0200 Subject: [PATCH 2/5] refactoring --- pkg/telemetry/phonehome/headers_multimap.go | 27 +++++++-------- .../phonehome/headers_multimap_test.go | 33 +++++++++---------- pkg/telemetry/phonehome/interceptor_test.go | 3 +- pkg/telemetry/phonehome/request_params.go | 5 +-- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/pkg/telemetry/phonehome/headers_multimap.go b/pkg/telemetry/phonehome/headers_multimap.go index 17ca1800f772e..e47eb2120ba09 100644 --- a/pkg/telemetry/phonehome/headers_multimap.go +++ b/pkg/telemetry/phonehome/headers_multimap.go @@ -31,7 +31,7 @@ func (h Headers) Get(key string) []string { // Returns nil if the key is absent or no values match the pattern. // For the special case where the key exists with no values and the pattern // matches empty string, returns a non-nil empty slice. -func (h Headers) GetMatching(key string, value glob.Pattern) []string { +func (h Headers) GetMatchingValues(key string, value glob.Pattern) []string { if h == nil { return nil } @@ -52,24 +52,25 @@ func (h Headers) GetMatching(key string, value glob.Pattern) []string { } // GetAll returns filtered map of the headers and their values. -func (h Headers) GetAll(canonicalKey glob.Pattern, value glob.Pattern) (map[string][]string, error) { - if err := canonicalKey.Compile(); err != nil { - return nil, err - } - if err := value.Compile(); err != nil { - return nil, err +func (h Headers) GetMatching(canonicalKey glob.Pattern, value glob.Pattern) map[string][]string { + if h == nil { + return nil } - result := make(map[string][]string) - for key, values := range h { + var result map[string][]string + for key := range h { if canonicalKey.Match(key) { - for _, v := range values { - if value == NoHeaderOrAnyValue || value.Match(v) { - result[key] = append(result[key], v) + matching := h.GetMatchingValues(key, value) + if matching != nil { + if result == nil { + result = make(map[string][]string) } + result[key] = matching } + } else if value == NoHeaderOrAnyValue && result == nil { + result = make(map[string][]string) } } - return result, nil + return result } // Set implements the setter interface. diff --git a/pkg/telemetry/phonehome/headers_multimap_test.go b/pkg/telemetry/phonehome/headers_multimap_test.go index 39c67272efdae..65027ec13d38e 100644 --- a/pkg/telemetry/phonehome/headers_multimap_test.go +++ b/pkg/telemetry/phonehome/headers_multimap_test.go @@ -46,7 +46,7 @@ func TestKeyCase(t *testing.T) { }) } -func TestGetMatching(t *testing.T) { +func TestGetMatchingValues(t *testing.T) { cases := map[string]struct { headers http.Header key string @@ -116,13 +116,13 @@ func TestGetMatching(t *testing.T) { } for name, tc := range cases { t.Run(name, func(t *testing.T) { - result := Headers(tc.headers).GetMatching(tc.key, tc.pattern) + result := Headers(tc.headers).GetMatchingValues(tc.key, tc.pattern) assert.Equal(t, tc.expected, result) }) } } -func TestGetAll(t *testing.T) { +func TestGetMatching_withKeyPattern(t *testing.T) { h := make(http.Header) h.Add("key-1", "value 1") h.Add("key-2", "value 2") @@ -131,25 +131,24 @@ func TestGetAll(t *testing.T) { h.Add("something-else", "value 3") headers := Headers(h) - matching, err := headers.GetAll("Key-*", "value 1") - assert.NoError(t, err) + matching := headers.GetMatching("Key-*", "value 1") assert.Equal(t, map[string][]string{"Key-1": {"value 1"}, "Key-2": {"value 1"}}, matching) - matching, err = headers.GetAll("nope", "value 1") - assert.NoError(t, err) - assert.Empty(t, matching) + matching = headers.GetMatching("nope", "value 1") + assert.Nil(t, matching) - matching, err = headers.GetAll("Key-1", "nope") - assert.NoError(t, err) - assert.Empty(t, matching) + matching = headers.GetMatching("Key-1", "nope") + assert.Nil(t, matching) - _, err = headers.GetAll("Key-[1-]", "nope") - assert.Error(t, err) + matching = headers.GetMatching("Key-[1-]", "nope") + assert.Nil(t, matching, "nil as bad pattern") - _, err = headers.GetAll("Key-1", "value [1-]") - assert.Error(t, err) + matching = headers.GetMatching("Key-1", "value [1-]") + assert.Nil(t, matching, "nil as bad pattern") - matching, err = headers.GetAll("*", "value [2-3]") - assert.NoError(t, err) + matching = headers.GetMatching("Key-??", NoHeaderOrAnyValue) + assert.Equal(t, map[string][]string{}, matching) + + matching = headers.GetMatching("*", "value [2-3]") assert.Equal(t, map[string][]string{"Something-Else": {"value 2", "value 3"}, "Key-2": {"value 2"}}, matching) } diff --git a/pkg/telemetry/phonehome/interceptor_test.go b/pkg/telemetry/phonehome/interceptor_test.go index b25b70c2937f7..7c8560db7c96c 100644 --- a/pkg/telemetry/phonehome/interceptor_test.go +++ b/pkg/telemetry/phonehome/interceptor_test.go @@ -164,8 +164,7 @@ func (s *interceptorTestSuite) TestGrpcRequestInfo() { s.NoError(err) s.Equal([]string{"test"}, ua) - matching, err := rp.Headers.GetAll("User-*", "*") - s.NoError(err) + matching := rp.Headers.GetMatching("User-*", "*") s.Equal(map[string][]string{"User-Agent": {"test"}}, matching) } diff --git a/pkg/telemetry/phonehome/request_params.go b/pkg/telemetry/phonehome/request_params.go index 86ddf1d37da0e..143562223831c 100644 --- a/pkg/telemetry/phonehome/request_params.go +++ b/pkg/telemetry/phonehome/request_params.go @@ -34,10 +34,7 @@ type GlobMap map[glob.Pattern]glob.Pattern func (rp *RequestParams) MatchHeaders(patterns GlobMap) Headers { result := make(Headers) for header, expression := range patterns { - matching, err := rp.Headers.GetAll(header, expression) - if err != nil { - return nil - } + matching := rp.Headers.GetMatching(header, expression) if matching == nil { if expression != NoHeaderOrAnyValue { return nil From f2435e76f139418b3b26e9989de46b5dc322cb81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Petrov?= Date: Wed, 1 Apr 2026 21:01:57 +0200 Subject: [PATCH 3/5] style --- pkg/telemetry/phonehome/campaign.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/telemetry/phonehome/campaign.go b/pkg/telemetry/phonehome/campaign.go index 6cc4cd96504fb..fdcf179898e68 100644 --- a/pkg/telemetry/phonehome/campaign.go +++ b/pkg/telemetry/phonehome/campaign.go @@ -95,7 +95,7 @@ func PathPattern(pattern glob.Pattern) *APICallCampaignCriterion { func HeaderPattern(header glob.Pattern, value glob.Pattern) *APICallCampaignCriterion { return &APICallCampaignCriterion{ Headers: GlobMap{ - glob.Pattern(header): glob.Pattern(value), + header: value, }, } } From aa50623600c7b92720254f2112766d13e674c43c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Petrov?= Date: Thu, 2 Apr 2026 14:54:05 +0200 Subject: [PATCH 4/5] typo --- pkg/telemetry/phonehome/campaign_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/telemetry/phonehome/campaign_test.go b/pkg/telemetry/phonehome/campaign_test.go index 1dbcf39bf9265..a56b5b8ce9aa2 100644 --- a/pkg/telemetry/phonehome/campaign_test.go +++ b/pkg/telemetry/phonehome/campaign_test.go @@ -137,7 +137,7 @@ func TestCampaignFulfilled(t *testing.T) { }, } expected := APICallCampaign{campaign[1], campaign[2], campaign[5]} - expecedHeaders := Headers{ + expectedHeaders := Headers{ userAgentHeaderKey: rp.Headers[userAgentHeaderKey], "X-Header-1": {"value 2"}, // "value 1" is not matched "X-Header-2": {"value 1", "value 2"}, @@ -152,7 +152,7 @@ func TestCampaignFulfilled(t *testing.T) { maps.Copy(matchedHeaders, h) })) assert.Equal(t, expected, fulfilled) - assert.Equal(t, expecedHeaders, matchedHeaders) + assert.Equal(t, expectedHeaders, matchedHeaders) }) t.Run("Missing headers", func(t *testing.T) { From 646fe3cf0c01fa37a7477c84b350431710b23052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Petrov?= Date: Thu, 2 Apr 2026 16:14:07 +0200 Subject: [PATCH 5/5] fixed inconsistent nil vs empty results --- pkg/telemetry/phonehome/headers_multimap.go | 40 ++++++++++--------- .../phonehome/headers_multimap_test.go | 6 +-- pkg/telemetry/phonehome/request_params.go | 11 ++++- .../phonehome/request_params_test.go | 7 ++++ 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/pkg/telemetry/phonehome/headers_multimap.go b/pkg/telemetry/phonehome/headers_multimap.go index e47eb2120ba09..6bdd66c4b2dcf 100644 --- a/pkg/telemetry/phonehome/headers_multimap.go +++ b/pkg/telemetry/phonehome/headers_multimap.go @@ -32,17 +32,17 @@ func (h Headers) Get(key string) []string { // For the special case where the key exists with no values and the pattern // matches empty string, returns a non-nil empty slice. func (h Headers) GetMatchingValues(key string, value glob.Pattern) []string { + var result []string + if value == NoHeaderOrAnyValue { + result = make([]string, 0) + } if h == nil { - return nil + return result } values, exists := http.Header(h)[http.CanonicalHeaderKey(key)] if !exists { - return nil - } - if len(values) == 0 && value.Match("") { - return make([]string, 0) + return result } - var result []string for _, v := range values { if value == NoHeaderOrAnyValue || value.Match(v) { result = append(result, v) @@ -53,22 +53,26 @@ func (h Headers) GetMatchingValues(key string, value glob.Pattern) []string { // GetAll returns filtered map of the headers and their values. func (h Headers) GetMatching(canonicalKey glob.Pattern, value glob.Pattern) map[string][]string { - if h == nil { - return nil - } var result map[string][]string + if value == NoHeaderOrAnyValue { + result = make(map[string][]string) + } for key := range h { - if canonicalKey.Match(key) { - matching := h.GetMatchingValues(key, value) - if matching != nil { - if result == nil { - result = make(map[string][]string) - } - result[key] = matching - } - } else if value == NoHeaderOrAnyValue && result == nil { + if !canonicalKey.Match(key) { + continue + } + matching := h.GetMatchingValues(key, value) + if matching == nil { + continue + } + if result == nil { result = make(map[string][]string) } + if existing, ok := result[key]; ok { + result[key] = append(existing, matching...) + } else { + result[key] = matching + } } return result } diff --git a/pkg/telemetry/phonehome/headers_multimap_test.go b/pkg/telemetry/phonehome/headers_multimap_test.go index 65027ec13d38e..eed328d70a7ad 100644 --- a/pkg/telemetry/phonehome/headers_multimap_test.go +++ b/pkg/telemetry/phonehome/headers_multimap_test.go @@ -57,13 +57,13 @@ func TestGetMatchingValues(t *testing.T) { headers: nil, key: "Missing", pattern: NoHeaderOrAnyValue, - expected: nil, + expected: []string{}, }, - "absent key returns nil regardless of pattern": { + "absent key returns empty on NoHeaderOrAnyValue": { headers: http.Header{}, key: "Missing", pattern: NoHeaderOrAnyValue, - expected: nil, + expected: []string{}, }, "key with no values, matching pattern returns empty slice": { headers: http.Header{"Key": {}}, diff --git a/pkg/telemetry/phonehome/request_params.go b/pkg/telemetry/phonehome/request_params.go index 143562223831c..416f64305cb43 100644 --- a/pkg/telemetry/phonehome/request_params.go +++ b/pkg/telemetry/phonehome/request_params.go @@ -2,7 +2,6 @@ package phonehome import ( "context" - "maps" "net/http" "github.com/stackrox/rox/pkg/glob" @@ -41,7 +40,15 @@ func (rp *RequestParams) MatchHeaders(patterns GlobMap) Headers { } continue } - maps.Copy(result, matching) + for k, v := range matching { + if existing, ok := result[k]; ok { + // Append appends nil instead of an empty array. That's why the + // else clause is needed. + result[k] = append(existing, v...) + } else { + result[k] = v + } + } } return result } diff --git a/pkg/telemetry/phonehome/request_params_test.go b/pkg/telemetry/phonehome/request_params_test.go index decc0fc5849b4..ef345c5e6a3f9 100644 --- a/pkg/telemetry/phonehome/request_params_test.go +++ b/pkg/telemetry/phonehome/request_params_test.go @@ -90,6 +90,13 @@ func TestMatchHeaders(t *testing.T) { }, expected: nil, }, + "multiple matching": { + headers: GlobMap{ + "Tw?": "one", + "?wo": "two", + }, + expected: Headers{"Two": {"one", "two"}}, + }, } for name, test := range tests { require.NoError(t, (&APICallCampaignCriterion{Headers: test.headers}).Compile())