Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions central/telemetry/centralclient/interceptors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/*}")
Expand All @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions central/telemetry/centralclient/interceptors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
}
Expand Down
13 changes: 8 additions & 5 deletions pkg/telemetry/phonehome/campaign.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
header: value,
},
}
}
95 changes: 93 additions & 2 deletions pkg/telemetry/phonehome/campaign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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) {
Expand Down Expand Up @@ -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{
Expand Down
40 changes: 33 additions & 7 deletions pkg/telemetry/phonehome/headers_multimap.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,18 @@ 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 {
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
return result
}
if len(values) == 0 && value.Match("") {
return make([]string, 0)
}
var result []string
for _, v := range values {
if value == NoHeaderOrAnyValue || value.Match(v) {
result = append(result, v)
Expand All @@ -51,6 +51,32 @@ 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) GetMatching(canonicalKey glob.Pattern, value glob.Pattern) map[string][]string {
var result map[string][]string
if value == NoHeaderOrAnyValue {
result = make(map[string][]string)
}
for key := range h {
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
}

// Set implements the setter interface.
func (h Headers) Set(key string, values ...string) {
for i, value := range values {
Expand Down
41 changes: 36 additions & 5 deletions pkg/telemetry/phonehome/headers_multimap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -57,13 +57,13 @@ func TestGetMatching(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": {}},
Expand Down Expand Up @@ -116,8 +116,39 @@ 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 TestGetMatching_withKeyPattern(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 := headers.GetMatching("Key-*", "value 1")
assert.Equal(t, map[string][]string{"Key-1": {"value 1"}, "Key-2": {"value 1"}}, matching)

matching = headers.GetMatching("nope", "value 1")
assert.Nil(t, matching)

matching = headers.GetMatching("Key-1", "nope")
assert.Nil(t, matching)

matching = headers.GetMatching("Key-[1-]", "nope")
assert.Nil(t, matching, "nil as bad pattern")

matching = headers.GetMatching("Key-1", "value [1-]")
assert.Nil(t, matching, "nil as bad pattern")

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)
}
3 changes: 1 addition & 2 deletions pkg/telemetry/phonehome/interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions pkg/telemetry/phonehome/interceptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ func (s *interceptorTestSuite) TestGrpcRequestInfo() {
ua := rp.Headers.Get("User-Agent")
s.NoError(err)
s.Equal([]string{"test"}, ua)

matching := rp.Headers.GetMatching("User-*", "*")
s.Equal(map[string][]string{"User-Agent": {"test"}}, matching)
}

func (s *interceptorTestSuite) TestGrpcWithHTTPRequestInfo() {
Expand Down
16 changes: 12 additions & 4 deletions pkg/telemetry/phonehome/request_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,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
Expand All @@ -33,14 +33,22 @@ 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 := rp.Headers.GetMatching(header, expression)
if matching == nil {
if expression != NoHeaderOrAnyValue {
return nil
}
continue
}
result[header] = values
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
}
Expand Down
12 changes: 10 additions & 2 deletions pkg/telemetry/phonehome/request_params_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package phonehome

import (
"net/http"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -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 {
Expand Down Expand Up @@ -89,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())
Expand Down
Loading