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
16 changes: 16 additions & 0 deletions central/config/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/stackrox/rox/central/config/datastore"
"github.com/stackrox/rox/central/convert/storagetov1"
"github.com/stackrox/rox/central/convert/v1tostorage"
customMetrics "github.com/stackrox/rox/central/metrics/custom"
"github.com/stackrox/rox/central/platform/matcher"
"github.com/stackrox/rox/central/platform/reprocessor"
"github.com/stackrox/rox/central/telemetry/centralclient"
Expand Down Expand Up @@ -131,6 +132,9 @@ func (s *serviceImpl) GetConfig(ctx context.Context, _ *v1.Empty) (*storage.Conf

// PutConfig updates Central's config
func (s *serviceImpl) PutConfig(ctx context.Context, req *v1.PutConfigRequest) (*storage.Config, error) {

// Validation:

if req.GetConfig() == nil {
return nil, errors.Wrap(errox.InvalidArgs, "config must be specified")
}
Expand Down Expand Up @@ -160,16 +164,28 @@ func (s *serviceImpl) PutConfig(ctx context.Context, req *v1.PutConfigRequest) (
regexes = append(regexes, regex)
}
}

if err := customMetrics.ValidateConfiguration(
req.GetConfig().GetPrivateConfig().GetMetrics()); err != nil {
return nil, err
}

// Store:

if err := s.datastore.UpsertConfig(ctx, req.GetConfig()); err != nil {
return nil, err
}

// Application:

if req.GetConfig().GetPublicConfig().GetTelemetry().GetEnabled() {
centralclient.Enable()
} else {
centralclient.Disable()
}
matcher.Singleton().SetRegexes(regexes)
go reprocessor.Singleton().RunReprocessor()

return req.GetConfig(), nil
}

Expand Down
26 changes: 26 additions & 0 deletions central/metrics/custom/image_vulnerabilities/labels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package image_vulnerabilities

import (
"github.com/stackrox/rox/central/metrics/custom/tracker"
"github.com/stackrox/rox/generated/storage"
)

var (
lazyLabels = []tracker.LazyLabel[finding]{
{Label: "Cluster", Getter: func(f *finding) string { return f.deployment.GetClusterName() }},
}

labels = tracker.MakeLabelOrderMap(lazyLabels)
)

// finding holds all information for computing any label in this category.
// The aggregator calls the lazy label's Getter function with every finding to
// compute the values for the list of defined labels.
type finding struct {
deployment *storage.Deployment
}

func ValidateConfiguration(config map[string]*storage.PrometheusMetrics_Group_Labels) error {
_, err := tracker.TranslateConfiguration(config, labels)
return err
}
11 changes: 11 additions & 0 deletions central/metrics/custom/singleton.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package aggregator

import (
"github.com/stackrox/rox/central/metrics/custom/image_vulnerabilities"
"github.com/stackrox/rox/generated/storage"
)

func ValidateConfiguration(config *storage.PrometheusMetrics) error {
return image_vulnerabilities.ValidateConfiguration(
config.GetImageVulnerabilities().GetDescriptors())
}
7 changes: 7 additions & 0 deletions central/metrics/custom/tracker/configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package tracker

type Label string // Prometheus label.
type MetricName string // Prometheus metric name.

// MetricsConfiguration is the parsed aggregation configuration.
type MetricsConfiguration map[MetricName][]Label
72 changes: 72 additions & 0 deletions central/metrics/custom/tracker/testing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package tracker

import (
"strings"
"testing"

"github.com/stackrox/rox/generated/storage"
)

// The test tracker finds some integers to track.
type testFinding int

var testLabelGetters = []LazyLabel[testFinding]{
testLabel("test"),
testLabel("Cluster"),
testLabel("Namespace"),
testLabel("CVE"),
testLabel("Severity"),
testLabel("CVSS"),
testLabel("IsFixable"),
}

var testLabelOrder = MakeLabelOrderMap(testLabelGetters)

func testLabel(label Label) LazyLabel[testFinding] {
return LazyLabel[testFinding]{
label,
func(i *testFinding) string { return testData[*i][label] }}
}

var testData = []map[Label]string{
{
"Severity": "CRITICAL",
"Cluster": "cluster 1",
"Namespace": "ns 1",
}, {
"Severity": "HIGH",
"Cluster": "cluster 2",
"Namespace": "ns 2",
},
{
"Severity": "LOW",
"Cluster": "cluster 3",
"Namespace": "ns 3",
},
{
"Severity": "CRITICAL",
"Cluster": "cluster 1",
"Namespace": "ns 3",
},
{
"Severity": "LOW",
"Cluster": "cluster 5",
"Namespace": "ns 3",
},
}

func makeTestMetricLabels(t *testing.T) map[string]*storage.PrometheusMetrics_Group_Labels {
pfx := strings.ReplaceAll(t.Name(), "/", "_")
return map[string]*storage.PrometheusMetrics_Group_Labels{
pfx + "_metric1": {Labels: []string{"Severity", "Cluster"}},
pfx + "_metric2": {Labels: []string{"Namespace"}},
}
}

func makeTestMetricConfiguration(t *testing.T) MetricsConfiguration {
pfx := MetricName(strings.ReplaceAll(t.Name(), "/", "_"))
return MetricsConfiguration{
pfx + "_metric1": {"Severity", "Cluster"},
pfx + "_metric2": {"Namespace"},
}
}
22 changes: 22 additions & 0 deletions central/metrics/custom/tracker/tracker_base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package tracker

// LazyLabel enables deferred evaluation of a label's value.
// Computing and storing values for all labels for every finding would be
// inefficient. Instead, the Getter function computes the value for this
// specific label only when provided with a finding.
type LazyLabel[Finding any] struct {
Label
Getter func(*Finding) string
}

// MakeLabelOrderMap maps labels to their order according to the order of
// the labels in the list of getters.
// Respecting the order is important for computing the aggregation key, which is
// a concatenation of label values.
func MakeLabelOrderMap[Finding any](getters []LazyLabel[Finding]) map[Label]int {
result := make(map[Label]int, len(getters))
for i, getter := range getters {
result[getter.Label] = i + 1
}
return result
}
19 changes: 19 additions & 0 deletions central/metrics/custom/tracker/tracker_base_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tracker

import (
"testing"

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

func TestMakeLabelOrderMap(t *testing.T) {
assert.Equal(t, map[Label]int{
"test": 1,
"Cluster": 2,
"Namespace": 3,
"CVE": 4,
"Severity": 5,
"CVSS": 6,
"IsFixable": 7,
}, testLabelOrder)
}
67 changes: 67 additions & 0 deletions central/metrics/custom/tracker/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package tracker

import (
"maps"
"regexp"
"slices"

"github.com/pkg/errors"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/errox"
)

var (
errInvalidConfiguration = errox.InvalidArgs.New("invalid configuration")

// Source: https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
metricNamePattern = regexp.MustCompile("^[a-zA-Z_:][a-zA-Z0-9_:]*$")
)

func isKnownLabel(label string, labelOrder map[Label]int) bool {
_, ok := labelOrder[Label(label)]
return ok
}

// validateMetricName ensures the name is alnum_.
func validateMetricName(name string) error {
if len(name) == 0 {
return errors.New("empty")
}
if !metricNamePattern.MatchString(name) {
return errors.New(`doesn't match "` + metricNamePattern.String() + `"`)
}
return nil
}

func validateLabels(labels []string, labelOrder map[Label]int, metricName string) ([]Label, error) {
if len(labels) == 0 {
return nil, errInvalidConfiguration.CausedByf("no labels specified for metric %q", metricName)
}
metricLabels := make([]Label, 0, len(labels))
for _, label := range labels {
if !isKnownLabel(label, labelOrder) {
return nil, errInvalidConfiguration.CausedByf("label %q for metric %q is not in the list of known labels %v", label,
metricName, slices.Sorted(maps.Keys(labelOrder)))
}
metricLabels = append(metricLabels, Label(label))
}
return metricLabels, nil
}

// TranslateConfiguration converts the storage object to the usable map,
// validating the values.
func TranslateConfiguration(config map[string]*storage.PrometheusMetrics_Group_Labels, labelOrder map[Label]int) (MetricsConfiguration, error) {
result := make(MetricsConfiguration, len(config))
for metricName, labels := range config {
if err := validateMetricName(metricName); err != nil {
return nil, errInvalidConfiguration.CausedByf(
"invalid metric name %q: %v", metricName, err)
}
metricLabels, err := validateLabels(labels.GetLabels(), labelOrder, metricName)
if err != nil {
return nil, err
}
result[MetricName(metricName)] = metricLabels
}
return result, nil
}
66 changes: 66 additions & 0 deletions central/metrics/custom/tracker/validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package tracker

import (
"testing"

"github.com/stackrox/rox/generated/storage"
"github.com/stretchr/testify/assert"
)

func TestTranslateConfiguration(t *testing.T) {
config := makeTestMetricLabels(t)
mcfg, err := TranslateConfiguration(config, testLabelOrder)
assert.NoError(t, err)
assert.Equal(t, makeTestMetricConfiguration(t), mcfg)
}

func Test_validateMetricName(t *testing.T) {
tests := map[string]string{
"good": "",
"not good": `doesn't match "^[a-zA-Z_:][a-zA-Z0-9_:]*$"`,
"": "empty",
"abc_defAZ0145609": "",
"not-good": `doesn't match "^[a-zA-Z_:][a-zA-Z0-9_:]*$"`,
}
for name, expected := range tests {
t.Run(name, func(t *testing.T) {
if err := validateMetricName(name); err != nil {
assert.Equal(t, expected, err.Error())
} else {
assert.Empty(t, expected)
}
})
}
}

func Test_noLabels(t *testing.T) {
for _, labels := range []*storage.PrometheusMetrics_Group_Labels{{Labels: []string{}}, {}, nil} {
config := map[string]*storage.PrometheusMetrics_Group_Labels{
"metric": labels,
}
mcfg, err := TranslateConfiguration(config, testLabelOrder)
assert.Equal(t, `invalid configuration: no labels specified for metric "metric"`, err.Error())
assert.Empty(t, mcfg)
}

mcfg, err := TranslateConfiguration(nil, testLabelOrder)
assert.NoError(t, err)
assert.Empty(t, mcfg)
}

func Test_parseErrors(t *testing.T) {
config := map[string]*storage.PrometheusMetrics_Group_Labels{
"metric1": {
Labels: []string{"unknown"},
},
}
mcfg, err := TranslateConfiguration(config, testLabelOrder)
assert.Equal(t, `invalid configuration: label "unknown" for metric "metric1" is not in the list of known labels [CVE CVSS Cluster IsFixable Namespace Severity test]`, err.Error())
assert.Empty(t, mcfg)

delete(config, "metric1")
config["met rick"] = nil
mcfg, err = TranslateConfiguration(config, testLabelOrder)
assert.Equal(t, `invalid configuration: invalid metric name "met rick": doesn't match "^[a-zA-Z_:][a-zA-Z0-9_:]*$"`, err.Error())
assert.Empty(t, mcfg)
}
Loading
Loading