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
46 changes: 17 additions & 29 deletions central/telemetry/centralclient/interceptors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package centralclient
import (
"strings"

"github.com/gobwas/glob"
"github.com/stackrox/rox/pkg/clientconn"
"github.com/stackrox/rox/pkg/glob"
"github.com/stackrox/rox/pkg/sync"
"github.com/stackrox/rox/pkg/telemetry/phonehome"
)
Expand All @@ -18,47 +18,39 @@ const (
)

var (
ignoredPaths = glob.MustCompile("{/v1/ping,/v1.PingService/Ping,/v1/metadata,/static/*}")
ignoredPaths = glob.Pattern("{/v1/ping,/v1.PingService/Ping,/v1/metadata,/static/*}")

permanentTelemetryCampaign = phonehome.APICallCampaign{
{
Headers: map[string]phonehome.Pattern{
Headers: map[string]glob.Pattern{
userAgentHeaderKey: "*roxctl*",
clientconn.RoxctlCommandHeader: phonehome.NoHeaderOrAnyValue,
clientconn.RoxctlCommandIndexHeader: phonehome.NoHeaderOrAnyValue,
clientconn.ExecutionEnvironment: phonehome.NoHeaderOrAnyValue,
},
},
{
Path: phonehome.Pattern("/v1/clusters").Ptr(),
Headers: map[string]phonehome.Pattern{
Path: glob.Pattern("/v1/clusters").Ptr(),
Headers: map[string]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,
},
},
{
// Capture requests from GitHub action user agents.
// See https://github.com/stackrox/central-login/blob/68785c129f3faba128d820cfe767558287be53a3/src/main.ts#L73
// and https://github.com/stackrox/roxctl-installer-action/blob/47fb4f5b275066b8322369e6e33fa010915b0d13/action.yml#L59.
Headers: map[string]phonehome.Pattern{
userAgentHeaderKey: phonehome.Pattern("*-GHA*"),
},
},
// Capture requests from GitHub action user agents.
// See https://github.com/stackrox/central-login/blob/68785c129f3faba128d820cfe767558287be53a3/src/main.ts#L73
// and https://github.com/stackrox/roxctl-installer-action/blob/47fb4f5b275066b8322369e6e33fa010915b0d13/action.yml#L59.
phonehome.HeaderPattern(userAgentHeaderKey, "*-GHA*"),
{
// Capture SBOM generation requests. Corresponding handler in central/image/service/http_handler.go.
Path: phonehome.Pattern("/api/v1/images/sbom").Ptr(),
Method: phonehome.Pattern("POST").Ptr(),
Headers: map[string]phonehome.Pattern{userAgentHeaderKey: phonehome.NoHeaderOrAnyValue},
},
{
// Capture Jenkins Plugin requests
Headers: map[string]phonehome.Pattern{
userAgentHeaderKey: "*stackrox-container-image-scanner*",
},
Path: glob.Pattern("/api/v1/images/sbom").Ptr(),
Method: glob.Pattern("POST").Ptr(),
Headers: map[string]glob.Pattern{userAgentHeaderKey: phonehome.NoHeaderOrAnyValue},
},
// Capture Jenkins Plugin requests
phonehome.HeaderPattern(userAgentHeaderKey, "*stackrox-container-image-scanner*"),
apiPathsCampaign(),
userAgentsCampaign(),
}
Expand All @@ -75,8 +67,8 @@ var (
func apiPathsCampaign() *phonehome.APICallCampaignCriterion {
if pattern := apiWhiteList.Setting(); pattern != "" {
return &phonehome.APICallCampaignCriterion{
Path: phonehome.Pattern("{" + pattern + "}").Ptr(),
Headers: map[string]phonehome.Pattern{
Path: glob.Pattern("{" + pattern + "}").Ptr(),
Headers: map[string]glob.Pattern{
userAgentHeaderKey: phonehome.NoHeaderOrAnyValue,
},
}
Expand All @@ -88,11 +80,7 @@ func apiPathsCampaign() *phonehome.APICallCampaignCriterion {
// environment variable.
func userAgentsCampaign() *phonehome.APICallCampaignCriterion {
if pattern := userAgentsList.Setting(); pattern != "" {
return &phonehome.APICallCampaignCriterion{
Headers: map[string]phonehome.Pattern{
userAgentHeaderKey: phonehome.Pattern("{" + pattern + "}"),
},
}
return phonehome.HeaderPattern(userAgentHeaderKey, "{"+pattern+"}")
}
return nil
}
Expand Down
8 changes: 2 additions & 6 deletions central/telemetry/centralclient/interceptors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,7 @@ func Test_apiCall(t *testing.T) {
},
}
require.NoError(t, permanentTelemetryCampaign.Compile())
anyTestEndpoint := &phonehome.APICallCampaignCriterion{
Path: phonehome.Pattern("*test*").Ptr(),
}
anyTestEndpoint := phonehome.PathPattern("*test*")
appendRuntimeCampaign(&phonehome.RuntimeConfig{
APICallCampaign: phonehome.APICallCampaign{anyTestEndpoint},
})
Expand Down Expand Up @@ -216,9 +214,7 @@ func Test_addCustomHeaders(t *testing.T) {
}, props)
})
t.Run("add header from the single criterion", func(t *testing.T) {
tc = append(tc, &phonehome.APICallCampaignCriterion{
Headers: map[string]phonehome.Pattern{"Custom-Header": ""},
})
tc = append(tc, phonehome.HeaderPattern("Custom-Header", ""))
require.NoError(t, tc.Compile())
rp := &phonehome.RequestParams{
Method: "GET",
Expand Down
16 changes: 8 additions & 8 deletions central/telemetry/centralclient/runtime_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ func Test_getRuntimeConfig(t *testing.T) {
assert.Equal(t, &phonehome.RuntimeConfig{
Key: "remotekey",
APICallCampaign: phonehome.APICallCampaign{
{Method: phonehome.Pattern("{put,delete}").Ptr()},
{Headers: map[string]phonehome.Pattern{"Accept-Encoding": phonehome.Pattern("*json*")}},
phonehome.MethodPattern("{put,delete}"),
phonehome.HeaderPattern("Accept-Encoding", "*json*"),
},
}, cfg)
}
Expand Down Expand Up @@ -87,8 +87,8 @@ func Test_reloadConfig(t *testing.T) {
assert.True(t, enable)
assert.Equal(t, rc.Key, config.StorageKey)
assert.Equal(t, append(permanentTelemetryCampaign,
&phonehome.APICallCampaignCriterion{Method: phonehome.Pattern("{put,delete}").Ptr()},
&phonehome.APICallCampaignCriterion{Headers: map[string]phonehome.Pattern{"Accept-Encoding": phonehome.Pattern("*json*")}},
phonehome.MethodPattern("{put,delete}"),
phonehome.HeaderPattern("Accept-Encoding", "*json*"),
), telemetryCampaign)
})

Expand All @@ -104,8 +104,8 @@ func Test_reloadConfig(t *testing.T) {
assert.True(t, enable)
assert.Equal(t, "anotherKey", config.StorageKey)
assert.Equal(t, append(permanentTelemetryCampaign,
&phonehome.APICallCampaignCriterion{Method: phonehome.Pattern("GET").Ptr()},
&phonehome.APICallCampaignCriterion{Path: phonehome.Pattern("*splunk*").Ptr()},
phonehome.MethodPattern("GET"),
phonehome.PathPattern("*splunk*"),
), telemetryCampaign)
})
t.Run("reload corrupted config", func(t *testing.T) {
Expand All @@ -116,8 +116,8 @@ func Test_reloadConfig(t *testing.T) {
assert.False(t, enable)
assert.Equal(t, "anotherKey", config.StorageKey)
assert.Equal(t, append(permanentTelemetryCampaign,
&phonehome.APICallCampaignCriterion{Method: phonehome.Pattern("GET").Ptr()},
&phonehome.APICallCampaignCriterion{Path: phonehome.Pattern("*splunk*").Ptr()},
phonehome.MethodPattern("GET"),
phonehome.PathPattern("*splunk*"),
), telemetryCampaign)
})
t.Run("reload config with DISABLED key", func(t *testing.T) {
Expand Down
44 changes: 44 additions & 0 deletions pkg/glob/pattern.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package glob

import (
"github.com/gobwas/glob"
"github.com/pkg/errors"
"github.com/stackrox/rox/pkg/sync"
)

// Pattern is expected to be a string with a glob pattern.
type Pattern string

var globCache = sync.Map{}

func (p *Pattern) Compile() error {
if p == nil {
return nil
}
_, err := p.compile()
return err
}

func (p *Pattern) compile() (glob.Glob, error) {
g, err := glob.Compile(string(*p))
if err != nil {
return nil, errors.WithMessagef(err, "failed to compile %q", string(*p))
}
globCache.Store(*p, g)
return g, nil
}

func (p *Pattern) Match(s string) bool {
v, ok := globCache.Load(*p)
if !ok {
var err error
if v, err = p.compile(); err != nil {
return false
}
}
return v.(glob.Glob).Match(s)
}

func (p Pattern) Ptr() *Pattern {
return &p
}
21 changes: 21 additions & 0 deletions pkg/glob/pattern_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package glob

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestPattern(t *testing.T) {
p := Pattern("*value*")

_, cached := globCache.Load(p)
assert.False(t, cached)

assert.True(t, p.Match("value"))

_, cached = globCache.Load(p)
assert.True(t, cached)

assert.True(t, p.Match("some value"))
}
39 changes: 32 additions & 7 deletions pkg/telemetry/phonehome/campaign.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import (
"slices"

"github.com/pkg/errors"
"github.com/stackrox/rox/pkg/glob"
)

// APICallCampaignCriterion defines a criterion for an API interception of a
// telemetry campaign. Requests parameters need to match all fields for the
// request to be tracked. Any request matches empty criterion.
type APICallCampaignCriterion struct {
Path *Pattern `json:"path,omitempty"`
Method *Pattern `json:"method,omitempty"`
Codes []int32 `json:"codes,omitempty"`
Headers map[string]Pattern `json:"headers,omitempty"`
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"`
}

// APICallCampaign defines an API interception telemetry campaign as a list of
Expand All @@ -27,14 +28,14 @@ func (c *APICallCampaignCriterion) Compile() error {
return nil
}
for _, pattern := range c.Headers {
if err := pattern.compile(); err != nil {
if err := pattern.Compile(); err != nil {
return errors.WithMessage(err, "error parsing header pattern")
}
}
if err := c.Path.compile(); err != nil {
if err := c.Path.Compile(); err != nil {
return errors.WithMessage(err, "error parsing path pattern")
}
if err := c.Method.compile(); err != nil {
if err := c.Method.Compile(); err != nil {
return errors.WithMessage(err, "error parsing methods pattern")
}
return nil
Expand Down Expand Up @@ -69,3 +70,27 @@ func (c APICallCampaign) CountFulfilled(rp *RequestParams, f func(cc *APICallCam
}
return fulfilled
}

// Codes builds a codes list criterion.
func Codes(codes ...int32) *APICallCampaignCriterion {
return &APICallCampaignCriterion{Codes: codes}
}

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

// PathPattern builds a path pattern criterion.
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 {
return &APICallCampaignCriterion{
Headers: map[string]glob.Pattern{
header: glob.Pattern(pattern),
},
}
}
Loading
Loading