-
Notifications
You must be signed in to change notification settings - Fork 174
ROX-28326: expose vulnerability metrics #15058
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
49da319
ad1bee3
6881879
1423912
408bf18
25e1362
5c91ad0
ffc5ac3
6fef275
a43b4f2
cafb650
920daac
78243ba
3917293
5547008
d47b94f
33c2f08
efdd356
7111505
cac9b4b
8833143
ed13535
c0c59ab
f67710a
a5d0135
9414701
d780930
a4ef364
6416ca7
dfe4d77
5c930c1
5c143ac
80b01a6
47ce9b1
f00ab88
c2b28c0
c961f2b
5e412e7
03dbdb6
5af869b
dd1a161
b0cb6da
666c0f2
a445c71
c092786
313a778
230fb46
5cb7a1d
1ef205f
84a91b7
e283db9
a7401c3
eb92621
af53fb0
c3ba955
7397978
be8a8cf
7a8c1a5
d21a272
7eaaf3e
67f6772
b3ec6a8
66c1662
af17142
8405b8b
ee180cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package common | ||
|
|
||
| import ( | ||
| "github.com/prometheus/client_golang/prometheus" | ||
| ) | ||
|
|
||
| // aggregatedRecord is a single gauge metric record. | ||
| type aggregatedRecord struct { | ||
| labels prometheus.Labels | ||
| total int | ||
| } | ||
|
Comment on lines
+7
to
+11
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cannot we reuse the type that is built-in prometheus library?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please suggest a type. |
||
|
|
||
| // aggregator computes the aggregation result. | ||
| type aggregator[Finding Countable] struct { | ||
| result map[MetricName]map[aggregationKey]*aggregatedRecord | ||
| mcfg MetricsConfiguration | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because MetricsConfiguration has methods and is used in other places as such. |
||
| labelOrder map[Label]int | ||
| getters map[Label]func(Finding) string | ||
| } | ||
|
|
||
| func makeAggregator[Finding Countable](mcfg MetricsConfiguration, labelOrder map[Label]int, getters map[Label]func(Finding) string) *aggregator[Finding] { | ||
| aggregated := make(map[MetricName]map[aggregationKey]*aggregatedRecord) | ||
| for metric := range mcfg { | ||
| aggregated[metric] = make(map[aggregationKey]*aggregatedRecord) | ||
| } | ||
| return &aggregator[Finding]{aggregated, mcfg, labelOrder, getters} | ||
| } | ||
|
|
||
| // count the finding in the aggregation result. | ||
| func (r *aggregator[Finding]) count(finding Finding) { | ||
| labelValue := func(label Label) string { | ||
| return r.getters[label](finding) | ||
| } | ||
| for metric, labels := range r.mcfg { | ||
| if key, labels := makeAggregationKey(labels, labelValue, r.labelOrder); key != "" { | ||
| if rec, ok := r.result[metric][key]; ok { | ||
| rec.total += finding.Count() | ||
| } else { | ||
| r.result[metric][key] = &aggregatedRecord{labels, finding.Count()} | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| package common | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/prometheus/client_golang/prometheus" | ||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| type testFinding struct { | ||
| OneOrMore | ||
| index int | ||
| } | ||
|
|
||
| func Test_aggregator(t *testing.T) { | ||
| getters := map[Label]func(testFinding) string{ | ||
| "Severity": func(tf testFinding) string { return testData[tf.index]["Severity"] }, | ||
| "Cluster": func(tf testFinding) string { return testData[tf.index]["Cluster"] }, | ||
| "Namespace": func(tf testFinding) string { return testData[tf.index]["Namespace"] }, | ||
| } | ||
| a := makeAggregator(makeTestMetricLabelExpression(t), testLabelOrder, getters) | ||
| assert.NotNil(t, a) | ||
| assert.Equal(t, map[MetricName]map[aggregationKey]*aggregatedRecord{ | ||
| "Test_aggregator_metric1": {}, | ||
| "Test_aggregator_metric2": {}, | ||
| }, a.result) | ||
|
|
||
| a.count(testFinding{index: 0}) | ||
|
|
||
| assert.Equal(t, map[MetricName]map[aggregationKey]*aggregatedRecord{ | ||
| "Test_aggregator_metric1": { | ||
| "cluster 1|CRITICAL": &aggregatedRecord{ | ||
| labels: prometheus.Labels{"Cluster": testData[0]["Cluster"], "Severity": testData[0]["Severity"]}, | ||
| total: 1, | ||
| }}, | ||
| "Test_aggregator_metric2": { | ||
| "ns 1": &aggregatedRecord{ | ||
| labels: prometheus.Labels{"Namespace": testData[0]["Namespace"]}, | ||
| total: 1, | ||
| }}, | ||
| }, a.result) | ||
|
|
||
| a.count(testFinding{index: 0}) | ||
|
|
||
| assert.Equal(t, map[MetricName]map[aggregationKey]*aggregatedRecord{ | ||
| "Test_aggregator_metric1": { | ||
| "cluster 1|CRITICAL": &aggregatedRecord{ | ||
| labels: prometheus.Labels{"Cluster": testData[0]["Cluster"], "Severity": testData[0]["Severity"]}, | ||
| total: 2, | ||
| }}, | ||
| "Test_aggregator_metric2": { | ||
| "ns 1": &aggregatedRecord{ | ||
| labels: prometheus.Labels{"Namespace": testData[0]["Namespace"]}, | ||
| total: 2, | ||
| }}, | ||
| }, a.result) | ||
|
|
||
| a.count(testFinding{index: 1}) | ||
|
|
||
| assert.Equal(t, map[MetricName]map[aggregationKey]*aggregatedRecord{ | ||
| "Test_aggregator_metric1": { | ||
| "cluster 1|CRITICAL": &aggregatedRecord{ | ||
| labels: prometheus.Labels{"Cluster": testData[0]["Cluster"], "Severity": testData[0]["Severity"]}, | ||
| total: 2, | ||
| }, | ||
| "cluster 2|HIGH": &aggregatedRecord{ | ||
| labels: prometheus.Labels{"Cluster": testData[1]["Cluster"], "Severity": testData[1]["Severity"]}, | ||
| total: 1, | ||
| }, | ||
| }, | ||
| "Test_aggregator_metric2": { | ||
| "ns 1": &aggregatedRecord{ | ||
| labels: prometheus.Labels{"Namespace": testData[0]["Namespace"]}, | ||
| total: 2, | ||
| }, | ||
| "ns 2": &aggregatedRecord{ | ||
| labels: prometheus.Labels{"Namespace": testData[1]["Namespace"]}, | ||
| total: 1, | ||
| }, | ||
| }, | ||
| }, a.result) | ||
|
|
||
| a.count(testFinding{OneOrMore: 5, index: 1}) | ||
|
|
||
| assert.Equal(t, map[MetricName]map[aggregationKey]*aggregatedRecord{ | ||
| "Test_aggregator_metric1": { | ||
| "cluster 1|CRITICAL": &aggregatedRecord{ | ||
| labels: prometheus.Labels{"Cluster": testData[0]["Cluster"], "Severity": testData[0]["Severity"]}, | ||
| total: 2, | ||
| }, | ||
| "cluster 2|HIGH": &aggregatedRecord{ | ||
| labels: prometheus.Labels{"Cluster": testData[1]["Cluster"], "Severity": testData[1]["Severity"]}, | ||
| total: 6, | ||
| }, | ||
| }, | ||
| "Test_aggregator_metric2": { | ||
| "ns 1": &aggregatedRecord{ | ||
| labels: prometheus.Labels{"Namespace": testData[0]["Namespace"]}, | ||
| total: 2, | ||
| }, | ||
| "ns 2": &aggregatedRecord{ | ||
| labels: prometheus.Labels{"Namespace": testData[1]["Namespace"]}, | ||
| total: 6, | ||
| }, | ||
| }, | ||
| }, a.result) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| package common | ||
|
|
||
| import ( | ||
| "context" | ||
| "iter" | ||
| "regexp" | ||
| "slices" | ||
|
|
||
| "github.com/pkg/errors" | ||
| v1 "github.com/stackrox/rox/generated/api/v1" | ||
| "github.com/stackrox/rox/pkg/logging" | ||
| ) | ||
|
|
||
| var ( | ||
| log = logging.CreateLogger(logging.ModuleForName("central_metrics"), 1) | ||
|
|
||
| // Source: https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels | ||
| metricNamePattern = regexp.MustCompile("^[a-zA-Z_:][a-zA-Z0-9_:]*$") | ||
| ) | ||
|
|
||
| type Label string // Prometheus label. | ||
| type MetricName string // Prometheus metric name. | ||
| type Countable interface{ Count() int } | ||
| type FindingGenerator[Finding Countable] func(context.Context, *v1.Query, MetricsConfiguration) iter.Seq[Finding] | ||
| type LabelGetter[Finding Countable] struct { | ||
| Label Label | ||
| Getter func(Finding) string | ||
| } | ||
|
|
||
| // MetricsConfiguration is the parsed aggregation configuration. | ||
| type MetricsConfiguration map[MetricName]map[Label]Expression | ||
|
|
||
| func (mcfg MetricsConfiguration) HasAnyLabelOf(labels []Label) bool { | ||
| for _, labelExpr := range mcfg { | ||
| for label := range labelExpr { | ||
| if slices.Contains(labels, label) { | ||
| return true | ||
| } | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
Comment on lines
+34
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This smells like we could use some optimization from https://pkg.go.dev/maps. Could we somehow smartly apply the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| // OneOrMore is a helper implementation of Countable interface, that counts 1 | ||
| // by default. Can be used as a base for other implementations. | ||
| type OneOrMore int | ||
|
|
||
| func (o OneOrMore) Count() int { | ||
| if o > 0 { | ||
| return int(o) | ||
| } | ||
| return 1 | ||
| } | ||
parametalol marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| type aggregationKey string // e.g. IMPORTANT_VULNERABILITY_SEVERITY|true | ||
|
|
||
| var ErrStopIterator = errors.New("stopped") | ||
|
|
||
| // 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 | ||
| } | ||
|
|
||
| // Bind4th binds the fourth function argument: | ||
| // | ||
| // f(a, b, c, d) == Bind4th(f, d)(a, b, c) | ||
| func Bind4th[A1 any, A2 any, A3 any, A4 any, RV any](f func(A1, A2, A3, A4) RV, bound A4) func(A1, A2, A3) RV { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make it a lib :)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| return func(a1 A1, a2 A2, a3 A3) RV { | ||
| return f(a1, a2, a3, bound) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something does not fit here right. The aggregator is a part of the config service, so I would expect that the config service would be started here, not the aggregator. Why cannot we keep the hierarchy here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The aggregator is not a part of the config service. The latter allows for reconfiguring the former in runtime.