Skip to content
Closed
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
31 changes: 14 additions & 17 deletions central/telemetry/centralclient/interceptors.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,14 @@ 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/*}")

permanentTelemetryCampaign = phonehome.APICallCampaign{
{
Headers: map[string]glob.Pattern{
Headers: map[glob.Pattern]glob.Pattern{
userAgentHeaderKey: "*roxctl*",
clientconn.RoxctlCommandHeader: phonehome.NoHeaderOrAnyValue,
clientconn.RoxctlCommandIndexHeader: phonehome.NoHeaderOrAnyValue,
Expand All @@ -30,12 +24,12 @@ var (
},
{
Path: glob.Pattern("/v1/clusters").Ptr(),
Headers: map[string]glob.Pattern{
Headers: map[glob.Pattern]glob.Pattern{
// 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 All @@ -46,7 +40,7 @@ var (
// Capture SBOM generation requests. Corresponding handler in central/image/service/http_handler.go.
Path: glob.Pattern("/api/v1/images/sbom").Ptr(),
Method: glob.Pattern("POST").Ptr(),
Headers: map[string]glob.Pattern{userAgentHeaderKey: phonehome.NoHeaderOrAnyValue},
Headers: map[glob.Pattern]glob.Pattern{userAgentHeaderKey: phonehome.NoHeaderOrAnyValue},
},
// Capture Jenkins Plugin requests
phonehome.HeaderPattern(userAgentHeaderKey, "*stackrox-container-image-scanner*"),
Expand All @@ -61,7 +55,7 @@ func apiPathsCampaign() *phonehome.APICallCampaignCriterion {
if pattern := apiWhiteList.Setting(); pattern != "" {
return &phonehome.APICallCampaignCriterion{
Path: glob.Pattern("{" + pattern + "}").Ptr(),
Headers: map[string]glob.Pattern{
Headers: map[glob.Pattern]glob.Pattern{
userAgentHeaderKey: phonehome.NoHeaderOrAnyValue,
},
}
Expand Down Expand Up @@ -103,13 +97,16 @@ func (c *CentralClient) apiCallInterceptor() phonehome.Interceptor {
// addCustomHeaders adds additional properties to the event if the telemetry
// campaign criterion contains a header pattern condition.
func addCustomHeaders(rp *phonehome.RequestParams, cc *phonehome.APICallCampaignCriterion, props map[string]any) {
if rp.Headers == nil || cc == nil {
if cc == nil {
return
}
for header := range cc.Headers {
values := rp.Headers(header)
if len(values) != 0 {
props[header] = strings.Join(values, "; ")
values, err := rp.Headers.GetAll(header, "*")
if err != nil {
return
}
for h, v := range values {
props[h] = strings.Join(v, "; ")
}
}
}
62 changes: 29 additions & 33 deletions central/telemetry/centralclient/interceptors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import (
"github.com/stretchr/testify/require"
)

func withUserAgent(_ *testing.T, headers map[string][]string, ua string) func(string) []string {
return func(key string) []string {
if http.CanonicalHeaderKey(key) == userAgentHeaderKey {
return []string{ua}
}
return headers[key]
// 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(_ *testing.T, headers http.Header, ua string) phonehome.Headers {
if headers == nil {
headers = make(http.Header)
}
headers.Set(userAgentHeaderKey, ua)
return phonehome.Headers(headers)
}

func Test_apiCall(t *testing.T) {
Expand Down Expand Up @@ -88,9 +91,10 @@ func Test_apiCall(t *testing.T) {
},
"ServiceNow from integration": {
rp: &phonehome.RequestParams{
Headers: withUserAgent(t, map[string][]string{
Headers: phonehome.Headers(http.Header{
snowIntegrationHeader: {"v1.0.3"},
}, "RHACS Integration ServiceNow client"),
"User-Agent": {"RHACS Integration ServiceNow client"},
}),
Method: "GET",
Path: "/v1/clusters",
Code: 200,
Expand Down Expand Up @@ -158,12 +162,10 @@ func Test_addCustomHeaders(t *testing.T) {
Method: "GET",
Path: "/v1/clusters",
Code: 200,
Headers: func(h string) []string {
return map[string][]string{
userAgentHeaderKey: {"RHACS Integration ServiceNow client"},
snowIntegrationHeader: {"v1.0.3", "beta"},
}[h]
},
Headers: phonehome.Headers(http.Header{
userAgentHeaderKey: {"RHACS Integration ServiceNow client"},
snowIntegrationHeader: {"v1.0.3", "beta"},
}),
}
props := map[string]any{}
addCustomHeaders(rp, tc[1], props)
Expand All @@ -177,12 +179,10 @@ func Test_addCustomHeaders(t *testing.T) {
Method: "GET",
Path: "/v1/clusters",
Code: 200,
Headers: func(h string) []string {
return map[string][]string{
userAgentHeaderKey: {"ServiceNow"},
"3rd-party-integration": {"v1.0.3", "beta"},
}[h]
},
Headers: phonehome.Headers(http.Header{
userAgentHeaderKey: {"ServiceNow"},
"3rd-Party-Integration": {"v1.0.3", "beta"},
}),
}
props := map[string]any{}
addCustomHeaders(rp, tc[1], props)
Expand All @@ -195,14 +195,12 @@ func Test_addCustomHeaders(t *testing.T) {
Method: "GET",
Path: "/v1/clusters",
Code: 200,
Headers: func(h string) []string {
return map[string][]string{
userAgentHeaderKey: {"roxctl"},
clientconn.RoxctlCommandHeader: {"central"},
clientconn.RoxctlCommandIndexHeader: {"1"},
clientconn.ExecutionEnvironment: {"github"},
}[h]
},
Headers: phonehome.Headers(http.Header{
userAgentHeaderKey: {"roxctl"},
clientconn.RoxctlCommandHeader: {"central"},
clientconn.RoxctlCommandIndexHeader: {"1"},
clientconn.ExecutionEnvironment: {"github"},
}),
}
props := map[string]any{}
addCustomHeaders(rp, tc[0], props)
Expand All @@ -220,11 +218,9 @@ func Test_addCustomHeaders(t *testing.T) {
Method: "GET",
Path: "/v1/config",
Code: 200,
Headers: func(h string) []string {
return map[string][]string{
userAgentHeaderKey: {"roxctl"},
}[h]
},
Headers: phonehome.Headers(http.Header{
userAgentHeaderKey: {"roxctl"},
}),
}
props := map[string]any{}
addCustomHeaders(rp, tc[0], props)
Expand Down
17 changes: 9 additions & 8 deletions pkg/telemetry/phonehome/campaign.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import (
// telemetry campaign. Requests parameters need to match all fields for the
// request to be tracked. Any request matches empty criterion.
type APICallCampaignCriterion struct {
Path *glob.Pattern `json:"path,omitempty"`
Method *glob.Pattern `json:"method,omitempty"`
Codes []int32 `json:"codes,omitempty"`
Headers map[string]glob.Pattern `json:"headers,omitempty"`
Path *glob.Pattern `json:"path,omitempty"`
Method *glob.Pattern `json:"method,omitempty"`
Codes []int32 `json:"codes,omitempty"`
Headers map[glob.Pattern]glob.Pattern `json:"headers,omitempty"`
}

// APICallCampaign defines an API interception telemetry campaign as a list of
Expand Down Expand Up @@ -86,11 +86,12 @@ func PathPattern(pattern string) *APICallCampaignCriterion {
return &APICallCampaignCriterion{Path: glob.Pattern(pattern).Ptr()}
}

// HeaderPattern builds a header pattern criterion.
func HeaderPattern(header string, pattern string) *APICallCampaignCriterion {
// HeaderPattern builds a header pattern criterion. Both header and value are
// glob patterns.
func HeaderPattern(header string, value string) *APICallCampaignCriterion {
return &APICallCampaignCriterion{
Headers: map[string]glob.Pattern{
header: glob.Pattern(pattern),
Headers: map[glob.Pattern]glob.Pattern{
glob.Pattern(header): glob.Pattern(value),
},
}
}
115 changes: 95 additions & 20 deletions pkg/telemetry/phonehome/campaign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import (
"github.com/stretchr/testify/require"
)

func withUserAgent(_ *testing.T, headers map[string][]string, ua string) func(string) []string {
return func(s string) []string {
if http.CanonicalHeaderKey(s) == userAgentHeaderKey {
return []string{ua}
}
return headers[s]
func withUserAgent(_ *testing.T, headers http.Header, ua string) Headers {
if headers == nil {
headers = make(http.Header)
}
headers.Set(userAgentHeaderKey, ua)
return Headers(headers)
}

func TestCampaignFulfilled(t *testing.T) {
Expand Down Expand Up @@ -117,33 +116,111 @@ func TestCampaignFulfilled(t *testing.T) {
require.NoError(t, campaign.Compile())
assert.Equal(t, 2, campaign.CountFulfilled(rp, doNothing))
})
t.Run("Header name and value globs", func(t *testing.T) {
headers := Headers(http.Header{
"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: map[glob.Pattern]glob.Pattern{
"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: map[glob.Pattern]glob.Pattern{"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("All criteria", func(t *testing.T) {
campaign := APICallCampaign{
{
Codes: []int32{200, 400},
Method: glob.Pattern("{GET,POST}").Ptr(),
Path: glob.Pattern("{/v1/test*,/v2/test*}").Ptr(),
Headers: map[string]glob.Pattern{"User-Agent": "*test*"},
Headers: map[glob.Pattern]glob.Pattern{"User-Agent": "*test*"},
},
{
Codes: []int32{200, 400},
Method: glob.Pattern("{GET,POST}").Ptr(),
Path: glob.Pattern("{/v1/test*,/v2/test*}").Ptr(),
Headers: map[string]glob.Pattern{"User-Agent": "*toast*"},
Headers: map[glob.Pattern]glob.Pattern{"User-Agent": "*toast*"},
},
{
Codes: []int32{300, 500},
Method: glob.Pattern("{DELETE,OPTIONS}").Ptr(),
Path: glob.Pattern("{/v3/test*,/v4/test*}").Ptr(),
Headers: map[string]glob.Pattern{"User-Agent": "{*tooth*,*teeth*}"},
Headers: map[glob.Pattern]glob.Pattern{"User-Agent": "{*tooth*,*teeth*}"},
},
{
Codes: []int32{100},
Method: glob.Pattern("PUT").Ptr(),
Path: glob.Pattern("/v5/*").Ptr(),
Headers: map[string]glob.Pattern{
Headers: map[glob.Pattern]glob.Pattern{
"User-Agent": "*another*",
"header": "val*",
"Header": "val*",
},
},
}
Expand Down Expand Up @@ -178,16 +255,14 @@ func TestCampaignFulfilled(t *testing.T) {
Method: "PUT",
Code: 100,
Path: "/v5/test",
Headers: func(h string) []string {
return map[string][]string{
userAgentHeaderKey: {"some another user-agent"},
"header": {"value"},
}[h]
},
Headers: Headers(http.Header{
userAgentHeaderKey: {"some another user-agent"},
"Header": {"value"},
}),
},
}
for _, rp := range rps {
assert.Equal(t, 1, campaign.CountFulfilled(&rp, doNothing), rp.Headers(userAgentHeaderKey))
assert.Equal(t, 1, campaign.CountFulfilled(&rp, doNothing), rp.Headers.Get(userAgentHeaderKey))
}
})

Expand Down Expand Up @@ -222,12 +297,12 @@ func TestCampaignFulfilled(t *testing.T) {
Path: "/v5/test/path",
Code: 100,
Headers: withUserAgent(t,
map[string][]string{"h": {"---"}},
http.Header{"H": {"---"}},
"some another user-agent 5"),
},
}
for _, rp := range rps {
assert.Zero(t, campaign.CountFulfilled(&rp, doNothing), rp.Headers(userAgentHeaderKey))
assert.Zero(t, campaign.CountFulfilled(&rp, doNothing), rp.Headers.Get(userAgentHeaderKey))
}
})
})
Expand Down
Loading
Loading