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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ controller webhooks will now be configured to 1) always scan images inline 2) ei
- `quay.io/openshift-release-dev/ocp-release`
- `quay.io/openshift-release-dev/ocp-v4.0-art-dev`

- ROX-28326: Custom Prometheus metrics exposed on the `/metrics` path of the central API endpoint. Configured via the `/v1/config` service.
Disabled by default.

### Removed Features

- ROX-30278: The `admissionControl.dynamic.timeout` configuration parameter of the secured-cluster-services Helm chart is not user-configurable anymore.
Expand Down
26 changes: 23 additions & 3 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 @@ -66,16 +67,18 @@ type Service interface {
}

// New returns a new Service instance using the given DataStore.
func New(datastore datastore.DataStore) Service {
func New(datastore datastore.DataStore, ar customMetrics.Runner) Service {
return &serviceImpl{
datastore: datastore,
datastore: datastore,
aggregator: ar,
}
}

type serviceImpl struct {
v1.UnimplementedConfigServiceServer

datastore datastore.DataStore
datastore datastore.DataStore
aggregator customMetrics.Runner
}

// RegisterServiceServer registers this service with the given gRPC Server.
Expand Down Expand Up @@ -131,6 +134,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 +166,30 @@ func (s *serviceImpl) PutConfig(ctx context.Context, req *v1.PutConfigRequest) (
regexes = append(regexes, regex)
}
}

customMetricsCfg, err := s.aggregator.ValidateConfiguration(
req.GetConfig().GetPrivateConfig().GetMetrics())
if 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()
s.aggregator.Reconfigure(customMetricsCfg)

return req.GetConfig(), nil
}

Expand Down
2 changes: 1 addition & 1 deletion central/config/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (s *configServiceTestSuite) SetupSuite() {
s.ctx = sac.WithAllAccess(context.Background())
s.db = pgtest.ForT(s.T())
s.dataStore = datastore.NewForTest(s.T(), s.db.DB)
s.srv = New(s.dataStore)
s.srv = New(s.dataStore, nil)

// Not found because Singleton() was not called and default configuration was not initialize.
cfg, err := s.srv.GetVulnerabilityExceptionConfig(s.ctx, &v1.Empty{})
Expand Down
3 changes: 2 additions & 1 deletion central/config/service/singleton.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package service

import (
"github.com/stackrox/rox/central/config/datastore"
customMetrics "github.com/stackrox/rox/central/metrics/custom"
"github.com/stackrox/rox/pkg/sync"
)

Expand All @@ -12,7 +13,7 @@ var (
)

func initialize() {
as = New(datastore.Singleton())
as = New(datastore.Singleton(), customMetrics.Singleton())
}

// Singleton provides the instance of the Service interface to register.
Expand Down
11 changes: 11 additions & 0 deletions central/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import (
"github.com/stackrox/rox/central/jwt"
logimbueHandler "github.com/stackrox/rox/central/logimbue/handler"
metadataService "github.com/stackrox/rox/central/metadata/service"
customMetrics "github.com/stackrox/rox/central/metrics/custom"
"github.com/stackrox/rox/central/metrics/telemetry"
mitreService "github.com/stackrox/rox/central/mitre/service"
namespaceService "github.com/stackrox/rox/central/namespace/service"
Expand Down Expand Up @@ -875,6 +876,16 @@ func customRoutes() (customRoutes []routes.CustomRoute) {
ServerHandler: certHandler.BackupCerts(listener.Singleton()),
Compression: true,
},
{
// User configured Prometheus metrics will be exposed on this path.
// The access is behind authorization because the metric label
// values may include sensitive data, such as deployment names and
// CVEs.
Route: "GET /metrics",
Authorizer: user.With(permissions.View(resources.Administration)),
ServerHandler: customMetrics.Singleton(),
Compression: true,
},
}
scannerDefinitionsRoute := "/api/extensions/scannerdefinitions"
// Only grant compression to well-known content types. It should capture files
Expand Down
50 changes: 50 additions & 0 deletions central/metrics/custom/image_vulnerabilities/labels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package image_vulnerabilities

import (
"strconv"

"github.com/stackrox/rox/central/metrics/custom/tracker"
"github.com/stackrox/rox/central/platform/matcher"
"github.com/stackrox/rox/generated/storage"
)

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

{Label: "ImageID", Getter: func(f *finding) string { return f.image.GetId() }},
{Label: "ImageRegistry", Getter: func(f *finding) string { return f.name.GetRegistry() }},
{Label: "ImageRemote", Getter: func(f *finding) string { return f.name.GetRemote() }},
{Label: "ImageTag", Getter: func(f *finding) string { return f.name.GetTag() }},
{Label: "Component", Getter: func(f *finding) string { return f.component.GetName() }},
{Label: "ComponentVersion", Getter: func(f *finding) string { return f.component.GetVersion() }},
{Label: "OperatingSystem", Getter: func(f *finding) string { return f.image.GetScan().GetOperatingSystem() }},

{Label: "CVE", Getter: func(f *finding) string { return f.vuln.GetCve() }},
{Label: "CVSS", Getter: func(f *finding) string { return strconv.FormatFloat(float64(f.vuln.GetCvss()), 'f', 1, 32) }},
{Label: "Severity", Getter: func(f *finding) string { return f.vuln.GetSeverity().String() }},
{Label: "SeverityV2", Getter: func(f *finding) string { return f.vuln.GetCvssV2().GetSeverity().String() }},
{Label: "SeverityV3", Getter: func(f *finding) string { return f.vuln.GetCvssV3().GetSeverity().String() }},
{Label: "IsFixable", Getter: func(f *finding) string { return strconv.FormatBool(f.vuln.GetFixedBy() != "") }},
}

// 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 {
err error
deployment *storage.Deployment
image *storage.Image
name *storage.ImageName
component *storage.EmbeddedImageScanComponent
vuln *storage.EmbeddedVulnerability
}

func (f *finding) GetError() error { return f.err }

func isPlatformWorkload(f *finding) string {
p, _ := matcher.Singleton().MatchDeployment(f.deployment)
return strconv.FormatBool(p)
}
63 changes: 63 additions & 0 deletions central/metrics/custom/image_vulnerabilities/tracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package image_vulnerabilities

import (
"context"
"iter"

deploymentDS "github.com/stackrox/rox/central/deployment/datastore"
"github.com/stackrox/rox/central/metrics"
"github.com/stackrox/rox/central/metrics/custom/tracker"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/sac"
"github.com/stackrox/rox/pkg/sac/resources"
"github.com/stackrox/rox/pkg/search"
)

func New(registry metrics.CustomRegistry, ds deploymentDS.DataStore) *tracker.TrackerBase[*finding] {
return tracker.MakeTrackerBase(
"vulnerabilities",
"aggregated CVEs",
lazyLabels,
func(ctx context.Context, mcfg tracker.MetricsConfiguration) iter.Seq[*finding] {
return trackVulnerabilityMetrics(ctx, mcfg, ds)
},
registry)
}

func trackVulnerabilityMetrics(ctx context.Context, _ tracker.MetricsConfiguration, ds deploymentDS.DataStore) iter.Seq[*finding] {
ctx = sac.WithGlobalAccessScopeChecker(ctx, sac.AllowFixedScopes(
sac.AccessModeScopeKeys(storage.Access_READ_ACCESS),
sac.ResourceScopeKeys(resources.Deployment, resources.Image)))

return func(yield func(*finding) bool) {
finding := &finding{}
_ = ds.WalkByQuery(ctx, search.EmptyQuery(), func(deployment *storage.Deployment) error {
finding.deployment = deployment
images, err := ds.GetImagesForDeployment(ctx, deployment)
if err != nil {
return nil // Nothing can be done with this error here.
}
for _, finding.image = range images {
if !forEachImageVuln(yield, finding) {
return tracker.ErrStopIterator
}
}
return nil
})
}
}

// forEachImageVuln yields a finding for every vulnerability associated with
// each image name.
func forEachImageVuln(yield func(*finding) bool, f *finding) bool {
for _, f.component = range f.image.GetScan().GetComponents() {
for _, f.vuln = range f.component.GetVulns() {
for _, f.name = range f.image.GetNames() {
if !yield(f) {
return false
}
}
}
}
return true
}
Loading
Loading