Skip to content
Merged
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
32 changes: 10 additions & 22 deletions central/telemetry/centralclient/interceptors.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
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"
snowIntegrationHeader = "Rh-Servicenow-Integration"

userAgentHeaderKey = "User-Agent"
)
Expand All @@ -21,7 +21,7 @@ var (

permanentTelemetryCampaign = phonehome.APICallCampaign{
{
Headers: map[string]glob.Pattern{
Headers: phonehome.GlobMap{
userAgentHeaderKey: "*roxctl*",
clientconn.RoxctlCommandHeader: phonehome.NoHeaderOrAnyValue,
clientconn.RoxctlCommandIndexHeader: phonehome.NoHeaderOrAnyValue,
Expand All @@ -30,7 +30,7 @@ var (
},
{
Path: glob.Pattern("/v1/clusters").Ptr(),
Headers: map[string]glob.Pattern{
Headers: phonehome.GlobMap{
// 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.
Expand All @@ -46,7 +46,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: phonehome.GlobMap{userAgentHeaderKey: phonehome.NoHeaderOrAnyValue},
},
// Capture Jenkins Plugin requests
phonehome.HeaderPattern(userAgentHeaderKey, "*stackrox-container-image-scanner*"),
Expand All @@ -61,7 +61,7 @@ func apiPathsCampaign() *phonehome.APICallCampaignCriterion {
if pattern := apiWhiteList.Setting(); pattern != "" {
return &phonehome.APICallCampaignCriterion{
Path: glob.Pattern("{" + pattern + "}").Ptr(),
Headers: map[string]glob.Pattern{
Headers: phonehome.GlobMap{
userAgentHeaderKey: phonehome.NoHeaderOrAnyValue,
},
}
Expand All @@ -73,7 +73,7 @@ func apiPathsCampaign() *phonehome.APICallCampaignCriterion {
// environment variable.
func userAgentsCampaign() *phonehome.APICallCampaignCriterion {
if pattern := userAgentsList.Setting(); pattern != "" {
return phonehome.HeaderPattern(userAgentHeaderKey, "{"+pattern+"}")
return phonehome.HeaderPattern(userAgentHeaderKey, glob.Pattern("{"+pattern+"}"))
}
return nil
}
Expand All @@ -94,22 +94,10 @@ func (c *CentralClient) apiCallInterceptor() phonehome.Interceptor {
c.campaignMux.RLock()
defer c.campaignMux.RUnlock()
return !ignoredPaths.Match(rp.Path) && c.telemetryCampaign.CountFulfilled(rp,
func(cc *phonehome.APICallCampaignCriterion) {
addCustomHeaders(rp, cc, props)
func(_ *phonehome.APICallCampaignCriterion, h phonehome.Headers) {
for k, values := range h {
props[k] = strings.Join(values, "; ")
}
}) > 0
}
}

// 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 {
return
}
for header := range cc.Headers {
values := rp.Headers(header)
if len(values) != 0 {
props[header] = strings.Join(values, "; ")
}
}
}
118 changes: 14 additions & 104 deletions central/telemetry/centralclient/interceptors_test.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
package centralclient

import (
"net/http"
"testing"

"github.com/stackrox/rox/pkg/clientconn"
"github.com/stackrox/rox/pkg/telemetry/phonehome"
"github.com/stretchr/testify/assert"
"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]
}
func withUserAgent(ua string) phonehome.Headers {
return phonehome.Headers{userAgentHeaderKey: {ua}}
}

func Test_apiCall(t *testing.T) {
Expand All @@ -28,7 +21,7 @@ func Test_apiCall(t *testing.T) {
}{
"roxctl": {
rp: &phonehome.RequestParams{
Headers: withUserAgent(t, nil, "Some roxctl client"),
Headers: withUserAgent("Some roxctl client"),
Method: "GET",
Path: "/v1/endpoint",
Code: 200,
Expand All @@ -38,7 +31,7 @@ func Test_apiCall(t *testing.T) {
},
"not roxctl": {
rp: &phonehome.RequestParams{
Headers: withUserAgent(t, nil, "Some other client"),
Headers: withUserAgent("Some other client"),
Method: "GET",
Path: "/v1/endpoint",
Code: 200,
Expand All @@ -48,7 +41,7 @@ func Test_apiCall(t *testing.T) {
},
"don't catch user-agent": {
rp: &phonehome.RequestParams{
Headers: withUserAgent(t, nil, "Some client"),
Headers: withUserAgent("Some client"),
Method: "GET",
Path: "/v1/test-endpoint",
Code: 200,
Expand All @@ -58,7 +51,7 @@ func Test_apiCall(t *testing.T) {
},
"roxctl ignored path": {
rp: &phonehome.RequestParams{
Headers: withUserAgent(t, nil, "Some roxctl client"),
Headers: withUserAgent("Some roxctl client"),
Method: "GET",
Path: "/v1/ping",
Code: 200,
Expand All @@ -68,7 +61,7 @@ func Test_apiCall(t *testing.T) {
},
"ServiceNow clusters": {
rp: &phonehome.RequestParams{
Headers: withUserAgent(t, nil, "Some ServiceNow client"),
Headers: withUserAgent("Some ServiceNow client"),
Method: "GET",
Path: "/v1/clusters",
Code: 200,
Expand All @@ -78,7 +71,7 @@ func Test_apiCall(t *testing.T) {
},
"ServiceNow deployments": {
rp: &phonehome.RequestParams{
Headers: withUserAgent(t, nil, "Some ServiceNow client"),
Headers: withUserAgent("Some ServiceNow client"),
Method: "GET",
Path: "/v1/deployments",
Code: 200,
Expand All @@ -88,9 +81,10 @@ func Test_apiCall(t *testing.T) {
},
"ServiceNow from integration": {
rp: &phonehome.RequestParams{
Headers: withUserAgent(t, map[string][]string{
Headers: phonehome.Headers{
snowIntegrationHeader: {"v1.0.3"},
}, "RHACS Integration ServiceNow client"),
"User-Agent": {"RHACS Integration ServiceNow client"},
},
Method: "GET",
Path: "/v1/clusters",
Code: 200,
Expand All @@ -103,7 +97,7 @@ func Test_apiCall(t *testing.T) {
},
"central-login GitHub action": {
rp: &phonehome.RequestParams{
Headers: withUserAgent(t, nil, "central-login-GHA"),
Headers: withUserAgent("central-login-GHA"),
Method: "POST",
Path: "/v1/auth/m2m/exchange",
},
Expand All @@ -114,7 +108,7 @@ func Test_apiCall(t *testing.T) {
},
"roxctl-installer GitHub action": {
rp: &phonehome.RequestParams{
Headers: withUserAgent(t, nil, "roxctl-installer-GHA"),
Headers: withUserAgent("roxctl-installer-GHA"),
Method: "GET",
Path: "/api/cli/download/roxctl-linux-amd64",
},
Expand All @@ -125,7 +119,7 @@ func Test_apiCall(t *testing.T) {
},
"SBOM generation": {
rp: &phonehome.RequestParams{
Headers: withUserAgent(t, nil, "Some SBOM client"),
Headers: withUserAgent("Some SBOM client"),
Method: "POST",
Path: "/api/v1/images/sbom",
Code: 200,
Expand All @@ -149,87 +143,3 @@ func Test_apiCall(t *testing.T) {
})
}
}

func Test_addCustomHeaders(t *testing.T) {
tc := permanentTelemetryCampaign
require.NoError(t, tc.Compile())
t.Run(snowIntegrationHeader, func(t *testing.T) {
rp := &phonehome.RequestParams{
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]
},
}
props := map[string]any{}
addCustomHeaders(rp, tc[1], props)
assert.Equal(t, map[string]any{
userAgentHeaderKey: "RHACS Integration ServiceNow client",
snowIntegrationHeader: "v1.0.3; beta",
}, props)
})
t.Run("3rd-party Integration", func(t *testing.T) {
rp := &phonehome.RequestParams{
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]
},
}
props := map[string]any{}
addCustomHeaders(rp, tc[1], props)
assert.Equal(t, map[string]any{
userAgentHeaderKey: "ServiceNow",
}, props)
})
t.Run("roxctl", func(t *testing.T) {
rp := &phonehome.RequestParams{
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]
},
}
props := map[string]any{}
addCustomHeaders(rp, tc[0], props)
assert.Equal(t, map[string]any{
userAgentHeaderKey: "roxctl",
clientconn.RoxctlCommandHeader: "central",
clientconn.RoxctlCommandIndexHeader: "1",
clientconn.ExecutionEnvironment: "github",
}, props)
})
t.Run("add header from the single criterion", func(t *testing.T) {
tc = append(tc, phonehome.HeaderPattern("Custom-Header", ""))
require.NoError(t, tc.Compile())
rp := &phonehome.RequestParams{
Method: "GET",
Path: "/v1/config",
Code: 200,
Headers: func(h string) []string {
return map[string][]string{
userAgentHeaderKey: {"roxctl"},
}[h]
},
}
props := map[string]any{}
addCustomHeaders(rp, tc[0], props)
assert.Equal(t, map[string]any{
userAgentHeaderKey: "roxctl",
}, props)
})
}
38 changes: 20 additions & 18 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 GlobMap `json:"headers,omitempty"`
}

// APICallCampaign defines an API interception telemetry campaign as a list of
Expand All @@ -41,12 +41,14 @@ func (c *APICallCampaignCriterion) Compile() error {
return nil
}

func (c *APICallCampaignCriterion) isFulfilled(rp *RequestParams) bool {
return c != nil &&
func (c *APICallCampaignCriterion) isFulfilled(rp *RequestParams) Headers {
if c != nil &&
(len(c.Codes) == 0 || slices.Contains(c.Codes, int32(rp.Code))) &&
(c.Path == nil || (*c.Path).Match(rp.Path)) &&
(c.Method == nil || (*c.Method).Match(rp.Method)) &&
(c.Headers == nil || rp.HasHeader(c.Headers))
(c.Method == nil || (*c.Method).Match(rp.Method)) {
return rp.MatchHeaders(c.Headers)
}
return nil
}

// Compile compiles and caches all glob patterns of the campaign.
Expand All @@ -60,11 +62,11 @@ func (c APICallCampaign) Compile() error {
}

// CountFulfilled calls f on each fulfilled criterion and returns their number.
func (c APICallCampaign) CountFulfilled(rp *RequestParams, f func(cc *APICallCampaignCriterion)) int {
func (c APICallCampaign) CountFulfilled(rp *RequestParams, f func(cc *APICallCampaignCriterion, h Headers)) int {
fulfilled := 0
for _, cc := range c {
if cc.isFulfilled(rp) {
f(cc)
if h := cc.isFulfilled(rp); h != nil {
f(cc, h)
fulfilled++
}
}
Expand All @@ -77,20 +79,20 @@ func Codes(codes ...int32) *APICallCampaignCriterion {
}

// MethodPattern builds a method pattern criterion.
func MethodPattern(pattern string) *APICallCampaignCriterion {
return &APICallCampaignCriterion{Method: glob.Pattern(pattern).Ptr()}
func MethodPattern(pattern glob.Pattern) *APICallCampaignCriterion {
return &APICallCampaignCriterion{Method: pattern.Ptr()}
}

// PathPattern builds a path pattern criterion.
func PathPattern(pattern string) *APICallCampaignCriterion {
return &APICallCampaignCriterion{Path: glob.Pattern(pattern).Ptr()}
func PathPattern(pattern glob.Pattern) *APICallCampaignCriterion {
return &APICallCampaignCriterion{Path: pattern.Ptr()}
}

// HeaderPattern builds a header pattern criterion.
func HeaderPattern(header string, pattern string) *APICallCampaignCriterion {
func HeaderPattern(header string, pattern glob.Pattern) *APICallCampaignCriterion {
return &APICallCampaignCriterion{
Headers: map[string]glob.Pattern{
header: glob.Pattern(pattern),
Headers: GlobMap{
header: pattern,
},
}
}
Loading
Loading