Repository: stackrox/stackrox
Pull request #14984: ROX-28519: Add methods for customizable platform components
Author: login: ajheflin
Labels: name: area/central
name: area/ui
Created: 2025-04-14T19:32:48Z | Merged: 2025-05-22T00:08:16Z

### Description

<!--
A detailed explanation of the changes in your PR. Feel free to remove this
section if the title of your PR is sufficiently descriptive. To learn more
about contributing to this project, check "*.md" files under:
    https://github.com/stackrox/stackrox/tree/master/.github
-->

This PR adds the datastore and service methods for updating and getting the platform component config.

These PRs are related to this:

- https://github.com/stackrox/stackrox/pull/14814
  - https://github.com/stackrox/stackrox/pull/14984 (This PR)
    - https://github.com/stackrox/stackrox/pull/15020

### User-facing documentation

- [ ] CHANGELOG is updated **OR** update is not needed
- [ ] [documentation PR](https://spaces.redhat.com/display/StackRox/Submitting+a+User+Documentation+Pull+Request) is created and is linked above **OR** is not needed

### Testing and quality

<!--
General Availability requirements: https://github.com/stackrox/stackrox/blob/master/PR_GA.md
Feature Flags usage: https://github.com/stackrox/stackrox/blob/master/pkg/features/README.md
-->

- [ ] the change is production ready: the change is GA or otherwise the functionality is gated by a feature flag
- [ ] CI results are inspected

#### Automated testing

<!--
If no tests have been contributed, please explain why unless it's obvious,
e.g., the PR is a one-line comment change.
-->

- [ ] added unit tests
- [ ] added e2e tests
- [ ] added regression tests
- [ ] added compatibility tests
- [ ] modified existing tests

#### How I validated my change

<!--
Use this space to explain **how you validated** that **your change functions
exactly how you expect it**. Feel free to attach JSON snippets, curl commands,
screenshots, etc. Apply a simple benchmark: would the information you provided
convince any reviewer or any external reader that you did enough to validate
your change.

It is acceptable to assume trust and keep this section light, e.g. as a
bullet-point list.

It is acceptable to skip testing in cases when CI is sufficient, or it's a
markdown or code comment change only. It is also acceptable to skip testing for
changes that are too taxing to test before merging. In such case you are
responsible for the change after it gets merged which includes reverting,
fixing, etc. Make sure you validate the change ASAP after it gets merged or
explain in PR when the validation will be performed. Explain here why you
skipped testing in case you did so.

Have you created automated tests for your change? Explain here which validation
activities you did manually and why so.
-->

Used curl to verify API requests/responses work as intended



Diff:
diff --git a/central/config/datastore/datastore.go b/central/config/datastore/datastore.go
index bf63bf3f398f1..1f4d07e745b64 100644
--- a/central/config/datastore/datastore.go
+++ b/central/config/datastore/datastore.go
@@ -3,15 +3,20 @@ package datastore
 import (
 	"context"
 	"reflect"
+	"slices"
+	"strings"
 	"testing"
 	"time"
 
 	"github.com/hashicorp/golang-lru/v2/expirable"
+	"github.com/pkg/errors"
 	"github.com/stackrox/rox/central/config/store"
 	pgStore "github.com/stackrox/rox/central/config/store/postgres"
 	"github.com/stackrox/rox/generated/storage"
+	"github.com/stackrox/rox/pkg/errox"
 	"github.com/stackrox/rox/pkg/postgres"
 	"github.com/stackrox/rox/pkg/protocompat"
+	"github.com/stackrox/rox/pkg/protoutils"
 	"github.com/stackrox/rox/pkg/sac"
 	"github.com/stackrox/rox/pkg/sac/resources"
 	"github.com/stackrox/rox/pkg/sync"
@@ -21,11 +26,14 @@ import (
 //
 //go:generate mockgen-wrapper
 type DataStore interface {
-	GetConfig(context.Context) (*storage.Config, error)
-	GetPrivateConfig(context.Context) (*storage.PrivateConfig, error)
+	GetConfig(ctx context.Context) (*storage.Config, error)
+	GetPrivateConfig(ctx context.Context) (*storage.PrivateConfig, error)
 	GetVulnerabilityExceptionConfig(ctx context.Context) (*storage.VulnerabilityExceptionConfig, error)
 	GetPublicConfig() (*storage.PublicConfig, error)
-	UpsertConfig(context.Context, *storage.Config) error
+	UpsertConfig(ctx context.Context, config *storage.Config) error
+
+	GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error)
+	UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error)
 }
 
 const (
@@ -194,6 +202,14 @@ func (d *datastoreImpl) UpsertConfig(ctx context.Context, config *storage.Config
 			clusterRetentionConf.LastUpdated = protocompat.TimestampNow()
 		}
 	}
+	if config.GetPlatformComponentConfig() != nil {
+		existingPlatformConf, _, _ := d.GetPlatformComponentConfig(ctx)
+		platformConfig, err := validateAndUpdatePlatformComponentConfig(existingPlatformConf, config.GetPlatformComponentConfig().GetRules())
+		if err != nil {
+			return err
+		}
+		config.PlatformComponentConfig = platformConfig
+	}
 
 	upsertErr := d.store.Upsert(ctx, config)
 	if upsertErr != nil {
@@ -225,3 +241,93 @@ func clusterRetentionConfigsEqual(c1 *storage.DecommissionedClusterRetentionConf
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, sac.ErrResourceAccessDenied
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found {
+		return nil, errors.Wrap(errox.NotFound, "System configuration not found")
+	} else if err != nil {
+		return nil, err
+	}
+
+	config.PlatformComponentConfig, err = validateAndUpdatePlatformComponentConfig(config.PlatformComponentConfig, rules)
+	if err != nil {
+		return nil, err
+	}
+	err = d.store.Upsert(ctx, config)
+	if err != nil {
+		return nil, err
+	}
+	return config.GetPlatformComponentConfig(), nil
+}
+
+func validateAndUpdatePlatformComponentConfig(config *storage.PlatformComponentConfig, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	systemRuleExists := false
+	layeredProductsRuleExists := false
+	parsedRules := make([]*storage.PlatformComponentConfig_Rule, 0)
+	for _, rule := range rules {
+		if rule.Name == defaultPlatformConfigSystemRule.Name && !strings.EqualFold(rule.NamespaceRule.Regex, defaultPlatformConfigSystemRule.NamespaceRule.Regex) {
+			// Prevent override of system rule
+			return nil, errors.New("System rule cannot be overwritten")
+		} else if rule.Name == defaultPlatformConfigSystemRule.Name && strings.EqualFold(rule.NamespaceRule.Regex, defaultPlatformConfigSystemRule.NamespaceRule.Regex) {
+			// If for some reason they're trying to duplicate the system rule, we prevent that
+			if systemRuleExists {
+				continue
+			}
+			systemRuleExists = true
+		}
+		if rule.Name == defaultPlatformConfigLayeredProductsRule.Name {
+			// If for some reason somebody makes a rule with an identical name to the layered products rule, we only take the first occurrence of it.
+			if layeredProductsRuleExists {
+				continue
+			}
+			layeredProductsRuleExists = true
+		}
+		parsedRules = append(parsedRules, rule)
+	}
+	// Add back in default rules if they weren't passed in by the user
+	if !systemRuleExists {
+		parsedRules = append(parsedRules, defaultPlatformConfigSystemRule)
+	}
+	if !layeredProductsRuleExists {
+		parsedRules = append(parsedRules, defaultPlatformConfigLayeredProductsRule)
+	}
+	if config == nil {
+		config = &storage.PlatformComponentConfig{
+			Rules:             parsedRules,
+			NeedsReevaluation: true,
+		}
+	} else {
+		slices.SortFunc(config.GetRules(), ruleNameSortFunc)
+		slices.SortFunc(parsedRules, ruleNameSortFunc)
+		if !protoutils.SlicesEqual(config.GetRules(), parsedRules) {
+			config.NeedsReevaluation = true
+		}
+		config.Rules = parsedRules
+	}
+	return config, nil
+}
+
+func ruleNameSortFunc(a *storage.PlatformComponentConfig_Rule, b *storage.PlatformComponentConfig_Rule) int {
+	return strings.Compare(a.Name, b.Name)
+}
diff --git a/central/config/datastore/datastore_test.go b/central/config/datastore/datastore_test.go
index 1a73c7ee9631c..725e8342af62f 100644
--- a/central/config/datastore/datastore_test.go
+++ b/central/config/datastore/datastore_test.go
@@ -209,6 +209,153 @@ func (s *configDataStoreTestSuite) TestAllowsUpdate() {
 	s.NotNil(updatedPublicConfig)
 }
 
+func (s *configDataStoreTestSuite) TestGetPlatformComponentConfig() {
+	s.storage.EXPECT().Get(gomock.Any()).Return(&storage.Config{
+		PublicConfig:  sampleConfig.PublicConfig,
+		PrivateConfig: sampleConfig.PrivateConfig,
+		PlatformComponentConfig: &storage.PlatformComponentConfig{
+			NeedsReevaluation: true,
+			Rules: []*storage.PlatformComponentConfig_Rule{
+				defaultPlatformConfigSystemRule,
+				defaultPlatformConfigLayeredProductsRule,
+			},
+		},
+	}, true, nil).Times(1)
+
+	platformConfig, _, err := s.dataStore.GetPlatformComponentConfig(s.hasReadCtx)
+	s.NoError(err, "expected no error trying to read with permissions")
+	s.NotNil(platformConfig)
+	s.True(platformConfig.NeedsReevaluation)
+	s.Equal(2, len(platformConfig.Rules))
+}
+
+func (s *configDataStoreTestSuite) TestUpsertPlatformComponentConfig() {
+	// Test when no update is required
+	s.storage.EXPECT().Get(gomock.Any()).Return(&storage.Config{
+		PublicConfig:  sampleConfig.PublicConfig,
+		PrivateConfig: sampleConfig.PrivateConfig,
+		PlatformComponentConfig: &storage.PlatformComponentConfig{
+			NeedsReevaluation: false,
+			Rules: []*storage.PlatformComponentConfig_Rule{
+				defaultPlatformConfigSystemRule,
+				defaultPlatformConfigLayeredProductsRule,
+			},
+		},
+	}, true, nil).Times(1)
+	s.storage.EXPECT().Upsert(gomock.Any(), gomock.Any()).Return(nil).Times(1)
+
+	config, err := s.dataStore.UpsertPlatformComponentConfigRules(s.hasWriteCtx, []*storage.PlatformComponentConfig_Rule{
+		defaultPlatformConfigSystemRule,
+		defaultPlatformConfigLayeredProductsRule,
+	})
+	s.NoError(err, "expected no error trying to upsert basic config")
+	s.NotNil(config)
+	s.False(config.NeedsReevaluation)
+	s.Equal(2, len(config.Rules))
+
+	// Test when a re-evaluation should be triggered
+	s.storage.EXPECT().Get(gomock.Any()).Return(&storage.Config{
+		PublicConfig:  sampleConfig.PublicConfig,
+		PrivateConfig: sampleConfig.PrivateConfig,
+		PlatformComponentConfig: &storage.PlatformComponentConfig{
+			NeedsReevaluation: false,
+			Rules: []*storage.PlatformComponentConfig_Rule{
+				defaultPlatformConfigSystemRule,
+				defaultPlatformConfigLayeredProductsRule,
+			},
+		},
+	}, true, nil).Times(1)
+	s.storage.EXPECT().Upsert(gomock.Any(), gomock.Any()).Return(nil).Times(1)
+	config, err = s.dataStore.UpsertPlatformComponentConfigRules(s.hasWriteCtx, []*storage.PlatformComponentConfig_Rule{
+		defaultPlatformConfigSystemRule,
+		defaultPlatformConfigLayeredProductsRule,
+		{
+			Name: "new rule",
+			NamespaceRule: &storage.PlatformComponentConfig_Rule_NamespaceRule{
+				Regex: ".*",
+			},
+		},
+	})
+	s.NoError(err, "expected no error when upserting new rule")
+	s.NotNil(config)
+	s.True(config.NeedsReevaluation)
+	s.Equal(3, len(config.Rules))
+
+	// Test updating a system rule a couple ways
+	s.storage.EXPECT().Get(gomock.Any()).Return(&storage.Config{
+		PublicConfig:  sampleConfig.PublicConfig,
+		PrivateConfig: sampleConfig.PrivateConfig,
+		PlatformComponentConfig: &storage.PlatformComponentConfig{
+			NeedsReevaluation: false,
+			Rules: []*storage.PlatformComponentConfig_Rule{
+				defaultPlatformConfigSystemRule,
+				defaultPlatformConfigLayeredProductsRule,
+			},
+		},
+	}, true, nil).Times(1)
+	config, err = s.dataStore.UpsertPlatformComponentConfigRules(s.hasWriteCtx, []*storage.PlatformComponentConfig_Rule{
+		defaultPlatformConfigSystemRule,
+		defaultPlatformConfigLayeredProductsRule,
+		{
+			Name: "system rule",
+			NamespaceRule: &storage.PlatformComponentConfig_Rule_NamespaceRule{
+				Regex: "not the system regex",
+			},
+		},
+	})
+	s.Error(err, "expected an error when trying to override the system regex")
+	s.Nil(config)
+
+	s.storage.EXPECT().Get(gomock.Any()).Return(&storage.Config{
+		PublicConfig:  sampleConfig.PublicConfig,
+		PrivateConfig: sampleConfig.PrivateConfig,
+		PlatformComponentConfig: &storage.PlatformComponentConfig{
+			NeedsReevaluation: false,
+			Rules: []*storage.PlatformComponentConfig_Rule{
+				defaultPlatformConfigSystemRule,
+				defaultPlatformConfigLayeredProductsRule,
+			},
+		},
+	}, true, nil).Times(1)
+	s.storage.EXPECT().Upsert(gomock.Any(), gomock.Any()).Return(nil).Times(1)
+	config, err = s.dataStore.UpsertPlatformComponentConfigRules(s.hasWriteCtx, []*storage.PlatformComponentConfig_Rule{
+		defaultPlatformConfigSystemRule,
+		defaultPlatformConfigLayeredProductsRule,
+		defaultPlatformConfigSystemRule,
+	})
+	s.NoError(err, "expected no error trying to add duplicate system rule")
+	s.NotNil(config)
+	s.False(config.NeedsReevaluation)
+	s.Equal(2, len(config.Rules))
+
+	// Test duplicating the layered products rule
+	s.storage.EXPECT().Get(gomock.Any()).Return(&storage.Config{
+		PublicConfig:  sampleConfig.PublicConfig,
+		PrivateConfig: sampleConfig.PrivateConfig,
+		PlatformComponentConfig: &storage.PlatformComponentConfig{
+			NeedsReevaluation: false,
+			Rules: []*storage.PlatformComponentConfig_Rule{
+				defaultPlatformConfigSystemRule,
+				defaultPlatformConfigLayeredProductsRule,
+			},
+		},
+	}, true, nil).Times(1)
+	s.storage.EXPECT().Upsert(gomock.Any(), gomock.Any()).Return(nil).Times(1)
+	config, err = s.dataStore.UpsertPlatformComponentConfigRules(s.hasWriteCtx, []*storage.PlatformComponentConfig_Rule{
+		defaultPlatformConfigSystemRule,
+		defaultPlatformConfigLayeredProductsRule,
+		{
+			Name: defaultPlatformConfigLayeredProductsRule.Name,
+			NamespaceRule: &storage.PlatformComponentConfig_Rule_NamespaceRule{
+				Regex: ".*",
+			},
+		},
+	})
+	s.NoError(err, "expected no error trying to add duplicate system rule")
+	s.NotNil(config)
+	s.Equal(2, len(config.Rules))
+}
+
 var (
 	customAlertRetention = &storage.PrivateConfig_AlertConfig{
 		AlertConfig: &storage.AlertRetentionConfig{
diff --git a/central/config/datastore/mocks/datastore.go b/central/config/datastore/mocks/datastore.go
index 516ddac27a90a..4fda169f6804d 100644
--- a/central/config/datastore/mocks/datastore.go
+++ b/central/config/datastore/mocks/datastore.go
@@ -42,33 +42,49 @@ func (m *MockDataStore) EXPECT() *MockDataStoreMockRecorder {
 }
 
 // GetConfig mocks base method.
-func (m *MockDataStore) GetConfig(arg0 context.Context) (*storage.Config, error) {
+func (m *MockDataStore) GetConfig(ctx context.Context) (*storage.Config, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetConfig", arg0)
+	ret := m.ctrl.Call(m, "GetConfig", ctx)
 	ret0, _ := ret[0].(*storage.Config)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // GetConfig indicates an expected call of GetConfig.
-func (mr *MockDataStoreMockRecorder) GetConfig(arg0 any) *gomock.Call {
+func (mr *MockDataStoreMockRecorder) GetConfig(ctx any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockDataStore)(nil).GetConfig), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockDataStore)(nil).GetConfig), ctx)
+}
+
+// GetPlatformComponentConfig mocks base method.
+func (m *MockDataStore) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetPlatformComponentConfig", ctx)
+	ret0, _ := ret[0].(*storage.PlatformComponentConfig)
+	ret1, _ := ret[1].(bool)
+	ret2, _ := ret[2].(error)
+	return ret0, ret1, ret2
+}
+
+// GetPlatformComponentConfig indicates an expected call of GetPlatformComponentConfig.
+func (mr *MockDataStoreMockRecorder) GetPlatformComponentConfig(ctx any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlatformComponentConfig", reflect.TypeOf((*MockDataStore)(nil).GetPlatformComponentConfig), ctx)
 }
 
 // GetPrivateConfig mocks base method.
-func (m *MockDataStore) GetPrivateConfig(arg0 context.Context) (*storage.PrivateConfig, error) {
+func (m *MockDataStore) GetPrivateConfig(ctx context.Context) (*storage.PrivateConfig, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetPrivateConfig", arg0)
+	ret := m.ctrl.Call(m, "GetPrivateConfig", ctx)
 	ret0, _ := ret[0].(*storage.PrivateConfig)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // GetPrivateConfig indicates an expected call of GetPrivateConfig.
-func (mr *MockDataStoreMockRecorder) GetPrivateConfig(arg0 any) *gomock.Call {
+func (mr *MockDataStoreMockRecorder) GetPrivateConfig(ctx any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateConfig", reflect.TypeOf((*MockDataStore)(nil).GetPrivateConfig), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateConfig", reflect.TypeOf((*MockDataStore)(nil).GetPrivateConfig), ctx)
 }
 
 // GetPublicConfig mocks base method.
@@ -102,15 +118,30 @@ func (mr *MockDataStoreMockRecorder) GetVulnerabilityExceptionConfig(ctx any) *g
 }
 
 // UpsertConfig mocks base method.
-func (m *MockDataStore) UpsertConfig(arg0 context.Context, arg1 *storage.Config) error {
+func (m *MockDataStore) UpsertConfig(ctx context.Context, config *storage.Config) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "UpsertConfig", arg0, arg1)
+	ret := m.ctrl.Call(m, "UpsertConfig", ctx, config)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // UpsertConfig indicates an expected call of UpsertConfig.
-func (mr *MockDataStoreMockRecorder) UpsertConfig(arg0, arg1 any) *gomock.Call {
+func (mr *MockDataStoreMockRecorder) UpsertConfig(ctx, config any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertConfig", reflect.TypeOf((*MockDataStore)(nil).UpsertConfig), ctx, config)
+}
+
+// UpsertPlatformComponentConfigRules mocks base method.
+func (m *MockDataStore) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UpsertPlatformComponentConfigRules", ctx, rules)
+	ret0, _ := ret[0].(*storage.PlatformComponentConfig)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// UpsertPlatformComponentConfigRules indicates an expected call of UpsertPlatformComponentConfigRules.
+func (mr *MockDataStoreMockRecorder) UpsertPlatformComponentConfigRules(ctx, rules any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertConfig", reflect.TypeOf((*MockDataStore)(nil).UpsertConfig), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertPlatformComponentConfigRules", reflect.TypeOf((*MockDataStore)(nil).UpsertPlatformComponentConfigRules), ctx, rules)
 }
diff --git a/central/config/service/service.go b/central/config/service/service.go
index 74414ccf4f0c2..cce8bc6a2c418 100644
--- a/central/config/service/service.go
+++ b/central/config/service/service.go
@@ -42,10 +42,12 @@ var (
 		user.With(permissions.View(resources.Administration)): {
 			v1.ConfigService_GetConfig_FullMethodName,
 			v1.ConfigService_GetPrivateConfig_FullMethodName,
+			v1.ConfigService_GetPlatformComponentConfig_FullMethodName,
 		},
 		user.With(permissions.Modify(resources.Administration)): {
 			v1.ConfigService_PutConfig_FullMethodName,
 			v1.ConfigService_UpdateVulnerabilityExceptionConfig_FullMethodName,
+			v1.ConfigService_UpdatePlatformComponentConfig_FullMethodName,
 		},
 	})
 )
@@ -200,6 +202,28 @@ func (s *serviceImpl) UpdateVulnerabilityExceptionConfig(ctx context.Context, re
 	}, nil
 }
 
+func (s *serviceImpl) GetPlatformComponentConfig(ctx context.Context, _ *v1.Empty) (*storage.PlatformComponentConfig, error) {
+	if !features.CustomizablePlatformComponents.Enabled() {
+		return nil, errors.Errorf("Cannot fulfill request. Environment variable %s=false", features.CustomizablePlatformComponents.EnvVar())
+	}
+	config, found, err := s.datastore.GetPlatformComponentConfig(ctx)
+	if !found || err != nil {
+		return nil, err
+	}
+	return config, nil
+}
+
+func (s *serviceImpl) UpdatePlatformComponentConfig(ctx context.Context, req *v1.PutPlatformComponentConfigRequest) (*storage.PlatformComponentConfig, error) {
+	if !features.CustomizablePlatformComponents.Enabled() {
+		return nil, errors.Errorf("Cannot fulfill request. Environment variable %s=false", features.CustomizablePlatformComponents.EnvVar())
+	}
+	config, err := s.datastore.UpsertPlatformComponentConfigRules(ctx, req.Rules)
+	if err != nil {
+		return nil, err
+	}
+	return config, nil
+}
+
 func validateExceptionConfigReq(config *storage.VulnerabilityExceptionConfig) error {
 	if config == nil {
 		return errors.Wrap(errox.InvalidArgs, "vulnerability exception config must be specified")
diff --git a/generated/api/v1/config_service.pb.go b/generated/api/v1/config_service.pb.go
index 96c1bf47d74e8..3337bd16de0d3 100644
--- a/generated/api/v1/config_service.pb.go
+++ b/generated/api/v1/config_service.pb.go
@@ -295,6 +295,50 @@ func (x *UpdateVulnerabilityExceptionConfigResponse) GetConfig() *VulnerabilityE
 	return nil
 }
 
+type PutPlatformComponentConfigRequest struct {
+	state         protoimpl.MessageState                  `protogen:"open.v1"`
+	Rules         []*storage.PlatformComponentConfig_Rule `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *PutPlatformComponentConfigRequest) Reset() {
+	*x = PutPlatformComponentConfigRequest{}
+	mi := &file_api_v1_config_service_proto_msgTypes[6]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *PutPlatformComponentConfigRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PutPlatformComponentConfigRequest) ProtoMessage() {}
+
+func (x *PutPlatformComponentConfigRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_api_v1_config_service_proto_msgTypes[6]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PutPlatformComponentConfigRequest.ProtoReflect.Descriptor instead.
+func (*PutPlatformComponentConfigRequest) Descriptor() ([]byte, []int) {
+	return file_api_v1_config_service_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *PutPlatformComponentConfigRequest) GetRules() []*storage.PlatformComponentConfig_Rule {
+	if x != nil {
+		return x.Rules
+	}
+	return nil
+}
+
 type VulnerabilityExceptionConfig_FixableCVEOptions struct {
 	state protoimpl.MessageState `protogen:"open.v1"`
 	// This options allows users to expire the vulnerability deferral request if and only if **all** vulnerabilities
@@ -309,7 +353,7 @@ type VulnerabilityExceptionConfig_FixableCVEOptions struct {
 
 func (x *VulnerabilityExceptionConfig_FixableCVEOptions) Reset() {
 	*x = VulnerabilityExceptionConfig_FixableCVEOptions{}
-	mi := &file_api_v1_config_service_proto_msgTypes[6]
+	mi := &file_api_v1_config_service_proto_msgTypes[7]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -321,7 +365,7 @@ func (x *VulnerabilityExceptionConfig_FixableCVEOptions) String() string {
 func (*VulnerabilityExceptionConfig_FixableCVEOptions) ProtoMessage() {}
 
 func (x *VulnerabilityExceptionConfig_FixableCVEOptions) ProtoReflect() protoreflect.Message {
-	mi := &file_api_v1_config_service_proto_msgTypes[6]
+	mi := &file_api_v1_config_service_proto_msgTypes[7]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -366,7 +410,7 @@ type VulnerabilityExceptionConfig_ExpiryOptions struct {
 
 func (x *VulnerabilityExceptionConfig_ExpiryOptions) Reset() {
 	*x = VulnerabilityExceptionConfig_ExpiryOptions{}
-	mi := &file_api_v1_config_service_proto_msgTypes[7]
+	mi := &file_api_v1_config_service_proto_msgTypes[8]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -378,7 +422,7 @@ func (x *VulnerabilityExceptionConfig_ExpiryOptions) String() string {
 func (*VulnerabilityExceptionConfig_ExpiryOptions) ProtoMessage() {}
 
 func (x *VulnerabilityExceptionConfig_ExpiryOptions) ProtoReflect() protoreflect.Message {
-	mi := &file_api_v1_config_service_proto_msgTypes[7]
+	mi := &file_api_v1_config_service_proto_msgTypes[8]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -453,12 +497,16 @@ const file_api_v1_config_service_proto_rawDesc = "" +
 	")UpdateVulnerabilityExceptionConfigRequest\x128\n" +
 	"\x06config\x18\x01 \x01(\v2 .v1.VulnerabilityExceptionConfigR\x06config\"f\n" +
 	"*UpdateVulnerabilityExceptionConfigResponse\x128\n" +
-	"\x06config\x18\x01 \x01(\v2 .v1.VulnerabilityExceptionConfigR\x06config2\x8b\x05\n" +
+	"\x06config\x18\x01 \x01(\v2 .v1.VulnerabilityExceptionConfigR\x06config\"`\n" +
+	"!PutPlatformComponentConfigRequest\x12;\n" +
+	"\x05rules\x18\x01 \x03(\v2%.storage.PlatformComponentConfig.RuleR\x05rules2\x90\a\n" +
 	"\rConfigService\x12N\n" +
 	"\x0fGetPublicConfig\x12\t.v1.Empty\x1a\x15.storage.PublicConfig\"\x19\x82\xd3\xe4\x93\x02\x13\x12\x11/v1/config/public\x12Q\n" +
 	"\x10GetPrivateConfig\x12\t.v1.Empty\x1a\x16.storage.PrivateConfig\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/v1/config/private\x12\x8f\x01\n" +
 	"\x1fGetVulnerabilityExceptionConfig\x12\t.v1.Empty\x1a+.v1.GetVulnerabilityExceptionConfigResponse\"4\x82\xd3\xe4\x93\x02.\x12,/v1/config/private/exception/vulnerabilities\x12\xbc\x01\n" +
-	"\"UpdateVulnerabilityExceptionConfig\x12-.v1.UpdateVulnerabilityExceptionConfigRequest\x1a..v1.UpdateVulnerabilityExceptionConfigResponse\"7\x82\xd3\xe4\x93\x021:\x01*\x1a,/v1/config/private/exception/vulnerabilities\x12;\n" +
+	"\"UpdateVulnerabilityExceptionConfig\x12-.v1.UpdateVulnerabilityExceptionConfigRequest\x1a..v1.UpdateVulnerabilityExceptionConfigResponse\"7\x82\xd3\xe4\x93\x021:\x01*\x1a,/v1/config/private/exception/vulnerabilities\x12o\n" +
+	"\x1aGetPlatformComponentConfig\x12\t.v1.Empty\x1a .storage.PlatformComponentConfig\"$\x82\xd3\xe4\x93\x02\x1e\x12\x1c/v1/config/platformcomponent\x12\x91\x01\n" +
+	"\x1dUpdatePlatformComponentConfig\x12%.v1.PutPlatformComponentConfigRequest\x1a .storage.PlatformComponentConfig\"'\x82\xd3\xe4\x93\x02!:\x01*\x1a\x1c/v1/config/platformcomponent\x12;\n" +
 	"\tGetConfig\x12\t.v1.Empty\x1a\x0f.storage.Config\"\x12\x82\xd3\xe4\x93\x02\f\x12\n" +
 	"/v1/config\x12I\n" +
 	"\tPutConfig\x12\x14.v1.PutConfigRequest\x1a\x0f.storage.Config\"\x15\x82\xd3\xe4\x93\x02\x0f:\x01*\x1a\n" +
@@ -477,7 +525,7 @@ func file_api_v1_config_service_proto_rawDescGZIP() []byte {
 	return file_api_v1_config_service_proto_rawDescData
 }
 
-var file_api_v1_config_service_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
+var file_api_v1_config_service_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
 var file_api_v1_config_service_proto_goTypes = []any{
 	(*PutConfigRequest)(nil),                               // 0: v1.PutConfigRequest
 	(*DayOption)(nil),                                      // 1: v1.DayOption
@@ -485,38 +533,46 @@ var file_api_v1_config_service_proto_goTypes = []any{
 	(*GetVulnerabilityExceptionConfigResponse)(nil),        // 3: v1.GetVulnerabilityExceptionConfigResponse
 	(*UpdateVulnerabilityExceptionConfigRequest)(nil),      // 4: v1.UpdateVulnerabilityExceptionConfigRequest
 	(*UpdateVulnerabilityExceptionConfigResponse)(nil),     // 5: v1.UpdateVulnerabilityExceptionConfigResponse
-	(*VulnerabilityExceptionConfig_FixableCVEOptions)(nil), // 6: v1.VulnerabilityExceptionConfig.FixableCVEOptions
-	(*VulnerabilityExceptionConfig_ExpiryOptions)(nil),     // 7: v1.VulnerabilityExceptionConfig.ExpiryOptions
-	(*storage.Config)(nil),                                 // 8: storage.Config
-	(*Empty)(nil),                                          // 9: v1.Empty
-	(*storage.PublicConfig)(nil),                           // 10: storage.PublicConfig
-	(*storage.PrivateConfig)(nil),                          // 11: storage.PrivateConfig
+	(*PutPlatformComponentConfigRequest)(nil),              // 6: v1.PutPlatformComponentConfigRequest
+	(*VulnerabilityExceptionConfig_FixableCVEOptions)(nil), // 7: v1.VulnerabilityExceptionConfig.FixableCVEOptions
+	(*VulnerabilityExceptionConfig_ExpiryOptions)(nil),     // 8: v1.VulnerabilityExceptionConfig.ExpiryOptions
+	(*storage.Config)(nil),                                 // 9: storage.Config
+	(*storage.PlatformComponentConfig_Rule)(nil),           // 10: storage.PlatformComponentConfig.Rule
+	(*Empty)(nil),                           // 11: v1.Empty
+	(*storage.PublicConfig)(nil),            // 12: storage.PublicConfig
+	(*storage.PrivateConfig)(nil),           // 13: storage.PrivateConfig
+	(*storage.PlatformComponentConfig)(nil), // 14: storage.PlatformComponentConfig
 }
 var file_api_v1_config_service_proto_depIdxs = []int32{
-	8,  // 0: v1.PutConfigRequest.config:type_name -> storage.Config
-	7,  // 1: v1.VulnerabilityExceptionConfig.expiry_options:type_name -> v1.VulnerabilityExceptionConfig.ExpiryOptions
+	9,  // 0: v1.PutConfigRequest.config:type_name -> storage.Config
+	8,  // 1: v1.VulnerabilityExceptionConfig.expiry_options:type_name -> v1.VulnerabilityExceptionConfig.ExpiryOptions
 	2,  // 2: v1.GetVulnerabilityExceptionConfigResponse.config:type_name -> v1.VulnerabilityExceptionConfig
 	2,  // 3: v1.UpdateVulnerabilityExceptionConfigRequest.config:type_name -> v1.VulnerabilityExceptionConfig
 	2,  // 4: v1.UpdateVulnerabilityExceptionConfigResponse.config:type_name -> v1.VulnerabilityExceptionConfig
-	1,  // 5: v1.VulnerabilityExceptionConfig.ExpiryOptions.day_options:type_name -> v1.DayOption
-	6,  // 6: v1.VulnerabilityExceptionConfig.ExpiryOptions.fixable_cve_options:type_name -> v1.VulnerabilityExceptionConfig.FixableCVEOptions
-	9,  // 7: v1.ConfigService.GetPublicConfig:input_type -> v1.Empty
-	9,  // 8: v1.ConfigService.GetPrivateConfig:input_type -> v1.Empty
-	9,  // 9: v1.ConfigService.GetVulnerabilityExceptionConfig:input_type -> v1.Empty
-	4,  // 10: v1.ConfigService.UpdateVulnerabilityExceptionConfig:input_type -> v1.UpdateVulnerabilityExceptionConfigRequest
-	9,  // 11: v1.ConfigService.GetConfig:input_type -> v1.Empty
-	0,  // 12: v1.ConfigService.PutConfig:input_type -> v1.PutConfigRequest
-	10, // 13: v1.ConfigService.GetPublicConfig:output_type -> storage.PublicConfig
-	11, // 14: v1.ConfigService.GetPrivateConfig:output_type -> storage.PrivateConfig
-	3,  // 15: v1.ConfigService.GetVulnerabilityExceptionConfig:output_type -> v1.GetVulnerabilityExceptionConfigResponse
-	5,  // 16: v1.ConfigService.UpdateVulnerabilityExceptionConfig:output_type -> v1.UpdateVulnerabilityExceptionConfigResponse
-	8,  // 17: v1.ConfigService.GetConfig:output_type -> storage.Config
-	8,  // 18: v1.ConfigService.PutConfig:output_type -> storage.Config
-	13, // [13:19] is the sub-list for method output_type
-	7,  // [7:13] is the sub-list for method input_type
-	7,  // [7:7] is the sub-list for extension type_name
-	7,  // [7:7] is the sub-list for extension extendee
-	0,  // [0:7] is the sub-list for field type_name
+	10, // 5: v1.PutPlatformComponentConfigRequest.rules:type_name -> storage.PlatformComponentConfig.Rule
+	1,  // 6: v1.VulnerabilityExceptionConfig.ExpiryOptions.day_options:type_name -> v1.DayOption
+	7,  // 7: v1.VulnerabilityExceptionConfig.ExpiryOptions.fixable_cve_options:type_name -> v1.VulnerabilityExceptionConfig.FixableCVEOptions
+	11, // 8: v1.ConfigService.GetPublicConfig:input_type -> v1.Empty
+	11, // 9: v1.ConfigService.GetPrivateConfig:input_type -> v1.Empty
+	11, // 10: v1.ConfigService.GetVulnerabilityExceptionConfig:input_type -> v1.Empty
+	4,  // 11: v1.ConfigService.UpdateVulnerabilityExceptionConfig:input_type -> v1.UpdateVulnerabilityExceptionConfigRequest
+	11, // 12: v1.ConfigService.GetPlatformComponentConfig:input_type -> v1.Empty
+	6,  // 13: v1.ConfigService.UpdatePlatformComponentConfig:input_type -> v1.PutPlatformComponentConfigRequest
+	11, // 14: v1.ConfigService.GetConfig:input_type -> v1.Empty
+	0,  // 15: v1.ConfigService.PutConfig:input_type -> v1.PutConfigRequest
+	12, // 16: v1.ConfigService.GetPublicConfig:output_type -> storage.PublicConfig
+	13, // 17: v1.ConfigService.GetPrivateConfig:output_type -> storage.PrivateConfig
+	3,  // 18: v1.ConfigService.GetVulnerabilityExceptionConfig:output_type -> v1.GetVulnerabilityExceptionConfigResponse
+	5,  // 19: v1.ConfigService.UpdateVulnerabilityExceptionConfig:output_type -> v1.UpdateVulnerabilityExceptionConfigResponse
+	14, // 20: v1.ConfigService.GetPlatformComponentConfig:output_type -> storage.PlatformComponentConfig
+	14, // 21: v1.ConfigService.UpdatePlatformComponentConfig:output_type -> storage.PlatformComponentConfig
+	9,  // 22: v1.ConfigService.GetConfig:output_type -> storage.Config
+	9,  // 23: v1.ConfigService.PutConfig:output_type -> storage.Config
+	16, // [16:24] is the sub-list for method output_type
+	8,  // [8:16] is the sub-list for method input_type
+	8,  // [8:8] is the sub-list for extension type_name
+	8,  // [8:8] is the sub-list for extension extendee
+	0,  // [0:8] is the sub-list for field type_name
 }
 
 func init() { file_api_v1_config_service_proto_init() }
@@ -531,7 +587,7 @@ func file_api_v1_config_service_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_config_service_proto_rawDesc), len(file_api_v1_config_service_proto_rawDesc)),
 			NumEnums:      0,
-			NumMessages:   8,
+			NumMessages:   9,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
diff --git a/generated/api/v1/config_service.pb.gw.go b/generated/api/v1/config_service.pb.gw.go
index ee47929b23e9f..14d6f41b15c00 100644
--- a/generated/api/v1/config_service.pb.gw.go
+++ b/generated/api/v1/config_service.pb.gw.go
@@ -116,6 +116,49 @@ func local_request_ConfigService_UpdateVulnerabilityExceptionConfig_0(ctx contex
 	return msg, metadata, err
 }
 
+func request_ConfigService_GetPlatformComponentConfig_0(ctx context.Context, marshaler runtime.Marshaler, client ConfigServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var (
+		protoReq Empty
+		metadata runtime.ServerMetadata
+	)
+	io.Copy(io.Discard, req.Body)
+	msg, err := client.GetPlatformComponentConfig(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+}
+
+func local_request_ConfigService_GetPlatformComponentConfig_0(ctx context.Context, marshaler runtime.Marshaler, server ConfigServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var (
+		protoReq Empty
+		metadata runtime.ServerMetadata
+	)
+	msg, err := server.GetPlatformComponentConfig(ctx, &protoReq)
+	return msg, metadata, err
+}
+
+func request_ConfigService_UpdatePlatformComponentConfig_0(ctx context.Context, marshaler runtime.Marshaler, client ConfigServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var (
+		protoReq PutPlatformComponentConfigRequest
+		metadata runtime.ServerMetadata
+	)
+	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+	msg, err := client.UpdatePlatformComponentConfig(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+}
+
+func local_request_ConfigService_UpdatePlatformComponentConfig_0(ctx context.Context, marshaler runtime.Marshaler, server ConfigServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var (
+		protoReq PutPlatformComponentConfigRequest
+		metadata runtime.ServerMetadata
+	)
+	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+	msg, err := server.UpdatePlatformComponentConfig(ctx, &protoReq)
+	return msg, metadata, err
+}
+
 func request_ConfigService_GetConfig_0(ctx context.Context, marshaler runtime.Marshaler, client ConfigServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
 	var (
 		protoReq Empty
@@ -245,6 +288,46 @@ func RegisterConfigServiceHandlerServer(ctx context.Context, mux *runtime.ServeM
 		}
 		forward_ConfigService_UpdateVulnerabilityExceptionConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
 	})
+	mux.Handle(http.MethodGet, pattern_ConfigService_GetPlatformComponentConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		var stream runtime.ServerTransportStream
+		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/v1.ConfigService/GetPlatformComponentConfig", runtime.WithHTTPPathPattern("/v1/config/platformcomponent"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_ConfigService_GetPlatformComponentConfig_0(annotatedContext, inboundMarshaler, server, req, pathParams)
+		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
+		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
+		if err != nil {
+			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		forward_ConfigService_GetPlatformComponentConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+	})
+	mux.Handle(http.MethodPut, pattern_ConfigService_UpdatePlatformComponentConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		var stream runtime.ServerTransportStream
+		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/v1.ConfigService/UpdatePlatformComponentConfig", runtime.WithHTTPPathPattern("/v1/config/platformcomponent"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_ConfigService_UpdatePlatformComponentConfig_0(annotatedContext, inboundMarshaler, server, req, pathParams)
+		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
+		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
+		if err != nil {
+			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		forward_ConfigService_UpdatePlatformComponentConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+	})
 	mux.Handle(http.MethodGet, pattern_ConfigService_GetConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
@@ -393,6 +476,40 @@ func RegisterConfigServiceHandlerClient(ctx context.Context, mux *runtime.ServeM
 		}
 		forward_ConfigService_UpdateVulnerabilityExceptionConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
 	})
+	mux.Handle(http.MethodGet, pattern_ConfigService_GetPlatformComponentConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/v1.ConfigService/GetPlatformComponentConfig", runtime.WithHTTPPathPattern("/v1/config/platformcomponent"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_ConfigService_GetPlatformComponentConfig_0(annotatedContext, inboundMarshaler, client, req, pathParams)
+		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
+		if err != nil {
+			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		forward_ConfigService_GetPlatformComponentConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+	})
+	mux.Handle(http.MethodPut, pattern_ConfigService_UpdatePlatformComponentConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/v1.ConfigService/UpdatePlatformComponentConfig", runtime.WithHTTPPathPattern("/v1/config/platformcomponent"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_ConfigService_UpdatePlatformComponentConfig_0(annotatedContext, inboundMarshaler, client, req, pathParams)
+		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
+		if err != nil {
+			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		forward_ConfigService_UpdatePlatformComponentConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+	})
 	mux.Handle(http.MethodGet, pattern_ConfigService_GetConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
@@ -435,6 +552,8 @@ var (
 	pattern_ConfigService_GetPrivateConfig_0                   = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "config", "private"}, ""))
 	pattern_ConfigService_GetVulnerabilityExceptionConfig_0    = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"v1", "config", "private", "exception", "vulnerabilities"}, ""))
 	pattern_ConfigService_UpdateVulnerabilityExceptionConfig_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"v1", "config", "private", "exception", "vulnerabilities"}, ""))
+	pattern_ConfigService_GetPlatformComponentConfig_0         = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "config", "platformcomponent"}, ""))
+	pattern_ConfigService_UpdatePlatformComponentConfig_0      = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "config", "platformcomponent"}, ""))
 	pattern_ConfigService_GetConfig_0                          = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "config"}, ""))
 	pattern_ConfigService_PutConfig_0                          = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "config"}, ""))
 )
@@ -444,6 +563,8 @@ var (
 	forward_ConfigService_GetPrivateConfig_0                   = runtime.ForwardResponseMessage
 	forward_ConfigService_GetVulnerabilityExceptionConfig_0    = runtime.ForwardResponseMessage
 	forward_ConfigService_UpdateVulnerabilityExceptionConfig_0 = runtime.ForwardResponseMessage
+	forward_ConfigService_GetPlatformComponentConfig_0         = runtime.ForwardResponseMessage
+	forward_ConfigService_UpdatePlatformComponentConfig_0      = runtime.ForwardResponseMessage
 	forward_ConfigService_GetConfig_0                          = runtime.ForwardResponseMessage
 	forward_ConfigService_PutConfig_0                          = runtime.ForwardResponseMessage
 )
diff --git a/generated/api/v1/config_service.swagger.json b/generated/api/v1/config_service.swagger.json
index c4ecb95328d1a..9b0d41b2b22cb 100644
--- a/generated/api/v1/config_service.swagger.json
+++ b/generated/api/v1/config_service.swagger.json
@@ -68,6 +68,58 @@
         ]
       }
     },
+    "/v1/config/platformcomponent": {
+      "get": {
+        "operationId": "ConfigService_GetPlatformComponentConfig",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/storagePlatformComponentConfig"
+            }
+          },
+          "default": {
+            "description": "An unexpected error response.",
+            "schema": {
+              "$ref": "#/definitions/googlerpcStatus"
+            }
+          }
+        },
+        "tags": [
+          "ConfigService"
+        ]
+      },
+      "put": {
+        "operationId": "ConfigService_UpdatePlatformComponentConfig",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/storagePlatformComponentConfig"
+            }
+          },
+          "default": {
+            "description": "An unexpected error response.",
+            "schema": {
+              "$ref": "#/definitions/googlerpcStatus"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/v1PutPlatformComponentConfigRequest"
+            }
+          }
+        ],
+        "tags": [
+          "ConfigService"
+        ]
+      }
+    },
     "/v1/config/private": {
       "get": {
         "operationId": "ConfigService_GetPrivateConfig",
@@ -507,6 +559,18 @@
         }
       }
     },
+    "v1PutPlatformComponentConfigRequest": {
+      "type": "object",
+      "properties": {
+        "rules": {
+          "type": "array",
+          "items": {
+            "type": "object",
+            "$ref": "#/definitions/PlatformComponentConfigRule"
+          }
+        }
+      }
+    },
     "v1UpdateVulnerabilityExceptionConfigRequest": {
       "type": "object",
       "properties": {
diff --git a/generated/api/v1/config_service_grpc.pb.go b/generated/api/v1/config_service_grpc.pb.go
index 0ce127a65080c..41d064ddf3e35 100644
--- a/generated/api/v1/config_service_grpc.pb.go
+++ b/generated/api/v1/config_service_grpc.pb.go
@@ -24,6 +24,8 @@ const (
 	ConfigService_GetPrivateConfig_FullMethodName                   = "/v1.ConfigService/GetPrivateConfig"
 	ConfigService_GetVulnerabilityExceptionConfig_FullMethodName    = "/v1.ConfigService/GetVulnerabilityExceptionConfig"
 	ConfigService_UpdateVulnerabilityExceptionConfig_FullMethodName = "/v1.ConfigService/UpdateVulnerabilityExceptionConfig"
+	ConfigService_GetPlatformComponentConfig_FullMethodName         = "/v1.ConfigService/GetPlatformComponentConfig"
+	ConfigService_UpdatePlatformComponentConfig_FullMethodName      = "/v1.ConfigService/UpdatePlatformComponentConfig"
 	ConfigService_GetConfig_FullMethodName                          = "/v1.ConfigService/GetConfig"
 	ConfigService_PutConfig_FullMethodName                          = "/v1.ConfigService/PutConfig"
 )
@@ -38,6 +40,8 @@ type ConfigServiceClient interface {
 	GetPrivateConfig(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*storage.PrivateConfig, error)
 	GetVulnerabilityExceptionConfig(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GetVulnerabilityExceptionConfigResponse, error)
 	UpdateVulnerabilityExceptionConfig(ctx context.Context, in *UpdateVulnerabilityExceptionConfigRequest, opts ...grpc.CallOption) (*UpdateVulnerabilityExceptionConfigResponse, error)
+	GetPlatformComponentConfig(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*storage.PlatformComponentConfig, error)
+	UpdatePlatformComponentConfig(ctx context.Context, in *PutPlatformComponentConfigRequest, opts ...grpc.CallOption) (*storage.PlatformComponentConfig, error)
 	GetConfig(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*storage.Config, error)
 	PutConfig(ctx context.Context, in *PutConfigRequest, opts ...grpc.CallOption) (*storage.Config, error)
 }
@@ -90,6 +94,26 @@ func (c *configServiceClient) UpdateVulnerabilityExceptionConfig(ctx context.Con
 	return out, nil
 }
 
+func (c *configServiceClient) GetPlatformComponentConfig(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*storage.PlatformComponentConfig, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(storage.PlatformComponentConfig)
+	err := c.cc.Invoke(ctx, ConfigService_GetPlatformComponentConfig_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *configServiceClient) UpdatePlatformComponentConfig(ctx context.Context, in *PutPlatformComponentConfigRequest, opts ...grpc.CallOption) (*storage.PlatformComponentConfig, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(storage.PlatformComponentConfig)
+	err := c.cc.Invoke(ctx, ConfigService_UpdatePlatformComponentConfig_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 func (c *configServiceClient) GetConfig(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*storage.Config, error) {
 	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
 	out := new(storage.Config)
@@ -120,6 +144,8 @@ type ConfigServiceServer interface {
 	GetPrivateConfig(context.Context, *Empty) (*storage.PrivateConfig, error)
 	GetVulnerabilityExceptionConfig(context.Context, *Empty) (*GetVulnerabilityExceptionConfigResponse, error)
 	UpdateVulnerabilityExceptionConfig(context.Context, *UpdateVulnerabilityExceptionConfigRequest) (*UpdateVulnerabilityExceptionConfigResponse, error)
+	GetPlatformComponentConfig(context.Context, *Empty) (*storage.PlatformComponentConfig, error)
+	UpdatePlatformComponentConfig(context.Context, *PutPlatformComponentConfigRequest) (*storage.PlatformComponentConfig, error)
 	GetConfig(context.Context, *Empty) (*storage.Config, error)
 	PutConfig(context.Context, *PutConfigRequest) (*storage.Config, error)
 }
@@ -143,6 +169,12 @@ func (UnimplementedConfigServiceServer) GetVulnerabilityExceptionConfig(context.
 func (UnimplementedConfigServiceServer) UpdateVulnerabilityExceptionConfig(context.Context, *UpdateVulnerabilityExceptionConfigRequest) (*UpdateVulnerabilityExceptionConfigResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method UpdateVulnerabilityExceptionConfig not implemented")
 }
+func (UnimplementedConfigServiceServer) GetPlatformComponentConfig(context.Context, *Empty) (*storage.PlatformComponentConfig, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetPlatformComponentConfig not implemented")
+}
+func (UnimplementedConfigServiceServer) UpdatePlatformComponentConfig(context.Context, *PutPlatformComponentConfigRequest) (*storage.PlatformComponentConfig, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method UpdatePlatformComponentConfig not implemented")
+}
 func (UnimplementedConfigServiceServer) GetConfig(context.Context, *Empty) (*storage.Config, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented")
 }
@@ -241,6 +273,42 @@ func _ConfigService_UpdateVulnerabilityExceptionConfig_Handler(srv interface{},
 	return interceptor(ctx, in, info, handler)
 }
 
+func _ConfigService_GetPlatformComponentConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(Empty)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(ConfigServiceServer).GetPlatformComponentConfig(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: ConfigService_GetPlatformComponentConfig_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(ConfigServiceServer).GetPlatformComponentConfig(ctx, req.(*Empty))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _ConfigService_UpdatePlatformComponentConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(PutPlatformComponentConfigRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(ConfigServiceServer).UpdatePlatformComponentConfig(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: ConfigService_UpdatePlatformComponentConfig_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(ConfigServiceServer).UpdatePlatformComponentConfig(ctx, req.(*PutPlatformComponentConfigRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 func _ConfigService_GetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 	in := new(Empty)
 	if err := dec(in); err != nil {
@@ -300,6 +368,14 @@ var ConfigService_ServiceDesc = grpc.ServiceDesc{
 			MethodName: "UpdateVulnerabilityExceptionConfig",
 			Handler:    _ConfigService_UpdateVulnerabilityExceptionConfig_Handler,
 		},
+		{
+			MethodName: "GetPlatformComponentConfig",
+			Handler:    _ConfigService_GetPlatformComponentConfig_Handler,
+		},
+		{
+			MethodName: "UpdatePlatformComponentConfig",
+			Handler:    _ConfigService_UpdatePlatformComponentConfig_Handler,
+		},
 		{
 			MethodName: "GetConfig",
 			Handler:    _ConfigService_GetConfig_Handler,
diff --git a/generated/api/v1/config_service_vtproto.pb.go b/generated/api/v1/config_service_vtproto.pb.go
index b60ea3a533464..5f590ec40df8d 100644
--- a/generated/api/v1/config_service_vtproto.pb.go
+++ b/generated/api/v1/config_service_vtproto.pb.go
@@ -173,6 +173,35 @@ func (m *UpdateVulnerabilityExceptionConfigResponse) CloneMessageVT() proto.Mess
 	return m.CloneVT()
 }
 
+func (m *PutPlatformComponentConfigRequest) CloneVT() *PutPlatformComponentConfigRequest {
+	if m == nil {
+		return (*PutPlatformComponentConfigRequest)(nil)
+	}
+	r := new(PutPlatformComponentConfigRequest)
+	if rhs := m.Rules; rhs != nil {
+		tmpContainer := make([]*storage.PlatformComponentConfig_Rule, len(rhs))
+		for k, v := range rhs {
+			if vtpb, ok := interface{}(v).(interface {
+				CloneVT() *storage.PlatformComponentConfig_Rule
+			}); ok {
+				tmpContainer[k] = vtpb.CloneVT()
+			} else {
+				tmpContainer[k] = proto.Clone(v).(*storage.PlatformComponentConfig_Rule)
+			}
+		}
+		r.Rules = tmpContainer
+	}
+	if len(m.unknownFields) > 0 {
+		r.unknownFields = make([]byte, len(m.unknownFields))
+		copy(r.unknownFields, m.unknownFields)
+	}
+	return r
+}
+
+func (m *PutPlatformComponentConfigRequest) CloneMessageVT() proto.Message {
+	return m.CloneVT()
+}
+
 func (this *PutConfigRequest) EqualVT(that *PutConfigRequest) bool {
 	if this == that {
 		return true
@@ -358,6 +387,45 @@ func (this *UpdateVulnerabilityExceptionConfigResponse) EqualMessageVT(thatMsg p
 	}
 	return this.EqualVT(that)
 }
+func (this *PutPlatformComponentConfigRequest) EqualVT(that *PutPlatformComponentConfigRequest) bool {
+	if this == that {
+		return true
+	} else if this == nil || that == nil {
+		return false
+	}
+	if len(this.Rules) != len(that.Rules) {
+		return false
+	}
+	for i, vx := range this.Rules {
+		vy := that.Rules[i]
+		if p, q := vx, vy; p != q {
+			if p == nil {
+				p = &storage.PlatformComponentConfig_Rule{}
+			}
+			if q == nil {
+				q = &storage.PlatformComponentConfig_Rule{}
+			}
+			if equal, ok := interface{}(p).(interface {
+				EqualVT(*storage.PlatformComponentConfig_Rule) bool
+			}); ok {
+				if !equal.EqualVT(q) {
+					return false
+				}
+			} else if !proto.Equal(p, q) {
+				return false
+			}
+		}
+	}
+	return string(this.unknownFields) == string(that.unknownFields)
+}
+
+func (this *PutPlatformComponentConfigRequest) EqualMessageVT(thatMsg proto.Message) bool {
+	that, ok := thatMsg.(*PutPlatformComponentConfigRequest)
+	if !ok {
+		return false
+	}
+	return this.EqualVT(that)
+}
 func (m *PutConfigRequest) MarshalVT() (dAtA []byte, err error) {
 	if m == nil {
 		return nil, nil
@@ -761,6 +829,63 @@ func (m *UpdateVulnerabilityExceptionConfigResponse) MarshalToSizedBufferVT(dAtA
 	return len(dAtA) - i, nil
 }
 
+func (m *PutPlatformComponentConfigRequest) MarshalVT() (dAtA []byte, err error) {
+	if m == nil {
+		return nil, nil
+	}
+	size := m.SizeVT()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBufferVT(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *PutPlatformComponentConfigRequest) MarshalToVT(dAtA []byte) (int, error) {
+	size := m.SizeVT()
+	return m.MarshalToSizedBufferVT(dAtA[:size])
+}
+
+func (m *PutPlatformComponentConfigRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
+	if m == nil {
+		return 0, nil
+	}
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.unknownFields != nil {
+		i -= len(m.unknownFields)
+		copy(dAtA[i:], m.unknownFields)
+	}
+	if len(m.Rules) > 0 {
+		for iNdEx := len(m.Rules) - 1; iNdEx >= 0; iNdEx-- {
+			if vtmsg, ok := interface{}(m.Rules[iNdEx]).(interface {
+				MarshalToSizedBufferVT([]byte) (int, error)
+			}); ok {
+				size, err := vtmsg.MarshalToSizedBufferVT(dAtA[:i])
+				if err != nil {
+					return 0, err
+				}
+				i -= size
+				i = protohelpers.EncodeVarint(dAtA, i, uint64(size))
+			} else {
+				encoded, err := proto.Marshal(m.Rules[iNdEx])
+				if err != nil {
+					return 0, err
+				}
+				i -= len(encoded)
+				copy(dAtA[i:], encoded)
+				i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded)))
+			}
+			i--
+			dAtA[i] = 0xa
+		}
+	}
+	return len(dAtA) - i, nil
+}
+
 func (m *PutConfigRequest) SizeVT() (n int) {
 	if m == nil {
 		return 0
@@ -895,6 +1020,28 @@ func (m *UpdateVulnerabilityExceptionConfigResponse) SizeVT() (n int) {
 	return n
 }
 
+func (m *PutPlatformComponentConfigRequest) SizeVT() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if len(m.Rules) > 0 {
+		for _, e := range m.Rules {
+			if size, ok := interface{}(e).(interface {
+				SizeVT() int
+			}); ok {
+				l = size.SizeVT()
+			} else {
+				l = proto.Size(e)
+			}
+			n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
+		}
+	}
+	n += len(m.unknownFields)
+	return n
+}
+
 func (m *PutConfigRequest) UnmarshalVT(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
@@ -1680,6 +1827,99 @@ func (m *UpdateVulnerabilityExceptionConfigResponse) UnmarshalVT(dAtA []byte) er
 	}
 	return nil
 }
+func (m *PutPlatformComponentConfigRequest) UnmarshalVT(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return protohelpers.ErrIntOverflow
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: PutPlatformComponentConfigRequest: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: PutPlatformComponentConfigRequest: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Rules", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return protohelpers.ErrIntOverflow
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Rules = append(m.Rules, &storage.PlatformComponentConfig_Rule{})
+			if unmarshal, ok := interface{}(m.Rules[len(m.Rules)-1]).(interface {
+				UnmarshalVT([]byte) error
+			}); ok {
+				if err := unmarshal.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil {
+					return err
+				}
+			} else {
+				if err := proto.Unmarshal(dAtA[iNdEx:postIndex], m.Rules[len(m.Rules)-1]); err != nil {
+					return err
+				}
+			}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := protohelpers.Skip(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
 func (m *PutConfigRequest) UnmarshalVTUnsafe(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
@@ -2465,3 +2705,96 @@ func (m *UpdateVulnerabilityExceptionConfigResponse) UnmarshalVTUnsafe(dAtA []by
 	}
 	return nil
 }
+func (m *PutPlatformComponentConfigRequest) UnmarshalVTUnsafe(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return protohelpers.ErrIntOverflow
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: PutPlatformComponentConfigRequest: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: PutPlatformComponentConfigRequest: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Rules", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return protohelpers.ErrIntOverflow
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Rules = append(m.Rules, &storage.PlatformComponentConfig_Rule{})
+			if unmarshal, ok := interface{}(m.Rules[len(m.Rules)-1]).(interface {
+				UnmarshalVTUnsafe([]byte) error
+			}); ok {
+				if err := unmarshal.UnmarshalVTUnsafe(dAtA[iNdEx:postIndex]); err != nil {
+					return err
+				}
+			} else {
+				if err := proto.Unmarshal(dAtA[iNdEx:postIndex], m.Rules[len(m.Rules)-1]); err != nil {
+					return err
+				}
+			}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := protohelpers.Skip(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
diff --git a/proto/api/v1/config_service.proto b/proto/api/v1/config_service.proto
index 9e09280f878c4..c34982957fe1a 100644
--- a/proto/api/v1/config_service.proto
+++ b/proto/api/v1/config_service.proto
@@ -53,6 +53,10 @@ message UpdateVulnerabilityExceptionConfigResponse {
   VulnerabilityExceptionConfig config = 1;
 }
 
+message PutPlatformComponentConfigRequest {
+  repeated storage.PlatformComponentConfig.Rule rules = 1;
+}
+
 // ConfigService APIs can be used to manage Central config.
 service ConfigService {
   rpc GetPublicConfig(Empty) returns (storage.PublicConfig) {
@@ -74,6 +78,17 @@ service ConfigService {
     };
   }
 
+  rpc GetPlatformComponentConfig(Empty) returns (storage.PlatformComponentConfig) {
+    option (google.api.http) = {get: "/v1/config/platformcomponent"};
+  }
+
+  rpc UpdatePlatformComponentConfig(PutPlatformComponentConfigRequest) returns (storage.PlatformComponentConfig) {
+    option (google.api.http) = {
+      put: "/v1/config/platformcomponent"
+      body: "*"
+    };
+  }
+
   rpc GetConfig(Empty) returns (storage.Config) {
     option (google.api.http) = {get: "/v1/config"};
   }


Reviews:
user: login: sachaudh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T21:57:20Z
user: login: ajheflin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T13:31:10Z
user: login: ajheflin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T13:32:39Z
user: login: ajheflin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T15:38:21Z
user: login: sourcery-ai[bot]
body: Hey @ajheflin - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider adding a comment to explain why `NeedsReevaluation` is set when the rules change.
- It would be helpful to add a comment explaining the purpose of the `PlatformComponentConfig` and its rules.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 2 issues found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Complexity**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T18:44:18Z
user: login: clickboo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T19:15:27Z
user: login: ajheflin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T19:20:52Z
user: login: dashrews78
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T19:11:15Z
user: login: dashrews78
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T19:18:10Z
user: login: vjwilson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T15:27:56Z
user: login: ajheflin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T17:14:46Z
user: login: ajheflin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T15:19:37Z
user: login: dashrews78
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:35:26Z
user: login: dashrews78
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:38:49Z
user: login: dashrews78
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:41:05Z
user: login: dashrews78
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:44:28Z
user: login: dashrews78
body: General comment, you should write some unit tests for these new functions.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:46:11Z
user: login: ajheflin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:46:20Z
user: login: ajheflin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:46:47Z
user: login: ajheflin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:47:13Z
user: login: charmik-redhat
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T13:53:41Z
user: login: charmik-redhat
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T14:26:14Z
user: login: ajheflin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T14:47:36Z
user: login: charmik-redhat
body: Couple of comments. Looks good otherwise
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T23:35:26Z
user: login: ajheflin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T13:53:45Z
user: login: ajheflin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T13:54:52Z
user: login: charmik-redhat
body: LGTM. Good unit tests
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T21:09:43Z

Review comments:
diff_hunk: @@ -74,6 +78,17 @@ service ConfigService {
     };
   }
 
+  rpc GetPlatformComponentConfig(Empty) returns (storage.PlatformComponentConfig) {
+    option (google.api.http) = {get: "/v1/config/platformcomponent"};
+  }
path: proto/api/v1/config_service.proto
user: login: sachaudh
body: Oh, interesting. I didn't realize we could get/modify each config (`public`, `private`, `platform component`) separately. I believe we're currently just using `/v1/config` in the UI. I'm assuming that `GET /v1/config` will include the new config, and that `PUT /v1/config` can also be used to update it. Let me know if I’ve got that right. If so, this looks good to me 👍🏼 
created_at: 2025-04-15T21:51:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,9 +79,22 @@ message PrivateConfig {
   AdministrationEventsConfig administration_events_config = 8;
 }
 
+message PlatformComponentConfig {
+  message Rule {
+    message NamespaceRule {
+      string regex = 1;
+    }
+    string name = 1;
+    NamespaceRule namespace_rule = 2;
+  }
+  repeated Rule rules = 1;
+  bool needs_reevaluation = 2;
path: proto/storage/config.proto
user: login: sachaudh
body: @ajheflin @charmik-redhat I see we have a state to determine whether there needs to be a re-evaluation. Could you give a quick explanation of this?

1. Is this telling the user they need to do something to re-evaluate? (Waiting for user action)
2. Will we re-evaluate automatically in the background and use this to determine if it's complete? (More like a status)
created_at: 2025-04-15T21:56:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,6 +78,17 @@ service ConfigService {
     };
   }
 
+  rpc GetPlatformComponentConfig(Empty) returns (storage.PlatformComponentConfig) {
+    option (google.api.http) = {get: "/v1/config/platformcomponent"};
+  }
path: proto/api/v1/config_service.proto
user: login: ajheflin
body: This should be the case, let me double check on that today and get back to you just to be sure, though
created_at: 2025-04-16T13:31:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,9 +79,22 @@ message PrivateConfig {
   AdministrationEventsConfig administration_events_config = 8;
 }
 
+message PlatformComponentConfig {
+  message Rule {
+    message NamespaceRule {
+      string regex = 1;
+    }
+    string name = 1;
+    NamespaceRule namespace_rule = 2;
+  }
+  repeated Rule rules = 1;
+  bool needs_reevaluation = 2;
path: proto/storage/config.proto
user: login: ajheflin
body: This is used by Central to determine if it needs to reprocess deployments. If the flag is set to true, it will reprocess them to re-organize the platform/non-platform components. There shouldn't be any action required on the user's part here, as once the reprocessing finishes that flag will be set to false
created_at: 2025-04-16T13:32:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,6 +78,17 @@ service ConfigService {
     };
   }
 
+  rpc GetPlatformComponentConfig(Empty) returns (storage.PlatformComponentConfig) {
+    option (google.api.http) = {get: "/v1/config/platformcomponent"};
+  }
path: proto/api/v1/config_service.proto
user: login: ajheflin
body: After 4ba1f44, the base config put endpoint works for this
created_at: 2025-04-16T15:38:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +250,94 @@
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	adminCtx := sac.WithGlobalAccessScopeChecker(context.Background(),
path: central/config/datastore/datastore.go
user: login: sourcery-ai[bot]
body: **suggestion:** Avoid using context.Background() in GetPlatformComponentConfig.

Using context.Background() here discards the caller’s context, including cancellation and deadline propagation. Consider using the passed-in context (or augmenting it) to ensure proper context propagation.

```suggestion
	adminCtx := sac.WithGlobalAccessScopeChecker(ctx,
```
created_at: 2025-04-16T18:44:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +250,94 @@
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	adminCtx := sac.WithGlobalAccessScopeChecker(context.Background(),
+		sac.AllowFixedScopes(
+			sac.AccessModeScopeKeys(storage.Access_READ_ACCESS),
+			sac.ResourceScopeKeys(resources.Administration)))
+
+	config, found, err := d.store.Get(adminCtx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRule(ctx context.Context, rule *storage.PlatformComponentConfig_Rule) error {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return err
+	} else if !ok {
+		return nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return err
+	}
+	if config.PlatformComponentConfig.Rules == nil {
+		config.PlatformComponentConfig.Rules = make([]*storage.PlatformComponentConfig_Rule, 0)
+	}
+	config.PlatformComponentConfig.Rules = append(config.PlatformComponentConfig.Rules, rule)
+	return d.store.Upsert(ctx, config)
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		log.Info("Error while checking permission")
+		return nil, err
path: central/config/datastore/datastore.go
user: login: sourcery-ai[bot]
body: **suggestion:** Review use of log.Info for permission errors.

For permission-related errors or access control issues, more appropriate logging levels (like warning or error) may improve clarity and alertness. Confirm that log levels align with the overall logging strategy.

Suggested implementation:

```golang
	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
		log.Error("Error while checking permission")

```

```golang
	} else if !ok {
		log.Warn("User did not have write access to the administration resource")

```
created_at: 2025-04-16T18:44:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,9 +79,22 @@ message PrivateConfig {
   AdministrationEventsConfig administration_events_config = 8;
 }
 
+message PlatformComponentConfig {
path: proto/storage/config.proto
user: login: clickboo
body: This proto seems over engineered.

message PlatformComponentConfig {
  message Rule {
         string name = 1;
         string regex = 2;
  }
  repeated Rule namespace_rules = 1;
   bool needs_reevaluation = 2;
}
created_at: 2025-04-16T19:15:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,9 +79,22 @@ message PrivateConfig {
   AdministrationEventsConfig administration_events_config = 8;
 }
 
+message PlatformComponentConfig {
path: proto/storage/config.proto
user: login: ajheflin
body: Most of the code revolving around the storage was from #14814, once that PR is merged the diff should be a bit more concise as to what this PR is actually for. Until then, the diff is [here](https://github.com/stackrox/stackrox/compare/6d21db174a66e35d74287d9ec4e204a3f8edf05a..e0179182d80a514cc48675361730529c93f071f1)
created_at: 2025-04-16T19:20:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +53,10 @@ message UpdateVulnerabilityExceptionConfigResponse {
   VulnerabilityExceptionConfig config = 1;
 }
 
+message PutPlatformComponentConfigRequest {
path: proto/api/v1/config_service.proto
user: login: dashrews78
body: I understand why you named this `Put`, but I think `Update` matches the other endpoints.
created_at: 2025-04-17T19:11:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -194,6 +201,19 @@ func (d *datastoreImpl) UpsertConfig(ctx context.Context, config *storage.Config
 			clusterRetentionConf.LastUpdated = protocompat.TimestampNow()
 		}
 	}
+	if platformComponentConfig := config.GetPlatformComponentConfig(); platformComponentConfig != nil {
+		oldConf, _, err := d.GetPlatformComponentConfig(ctx)
+		if err != nil {
+			return err
+		}
+		if oldConf != nil {
+			if !protoutils.SlicesEqual(oldConf.GetRules(), config.GetPlatformComponentConfig().GetRules()) {
path: central/config/datastore/datastore.go
user: login: dashrews78
body: Does this account for sliced simply ordered differently?
created_at: 2025-04-17T19:18:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +53,10 @@ message UpdateVulnerabilityExceptionConfigResponse {
   VulnerabilityExceptionConfig config = 1;
 }
 
+message PutPlatformComponentConfigRequest {
path: proto/api/v1/config_service.proto
user: login: vjwilson
body: The "Put" vs "Update" terminology is even more confused than that.

Based on an internal support question about our API docs, I have discovered that, in the imageintegration service, we [use "Put" for PUT](https://github.com/stackrox/stackrox/blob/master/central/imageintegration/service/service_impl.go#L148-L151), and ["Update" for PATCH](https://github.com/stackrox/stackrox/blob/master/central/imageintegration/service/service_impl.go#L148-L151). 

What's worse, we don't use PATCH there to replace partial properties, but for the stated reason ", with optional stored credential reconciliation". 😱 


created_at: 2025-04-18T15:27:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -194,6 +201,19 @@ func (d *datastoreImpl) UpsertConfig(ctx context.Context, config *storage.Config
 			clusterRetentionConf.LastUpdated = protocompat.TimestampNow()
 		}
 	}
+	if platformComponentConfig := config.GetPlatformComponentConfig(); platformComponentConfig != nil {
+		oldConf, _, err := d.GetPlatformComponentConfig(ctx)
+		if err != nil {
+			return err
+		}
+		if oldConf != nil {
+			if !protoutils.SlicesEqual(oldConf.GetRules(), config.GetPlatformComponentConfig().GetRules()) {
path: central/config/datastore/datastore.go
user: login: ajheflin
body: After a test of this, it does not, so I'll have to make a new way of checking this. Good catch.
created_at: 2025-05-12T17:14:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -194,6 +201,19 @@ func (d *datastoreImpl) UpsertConfig(ctx context.Context, config *storage.Config
 			clusterRetentionConf.LastUpdated = protocompat.TimestampNow()
 		}
 	}
+	if platformComponentConfig := config.GetPlatformComponentConfig(); platformComponentConfig != nil {
+		oldConf, _, err := d.GetPlatformComponentConfig(ctx)
+		if err != nil {
+			return err
+		}
+		if oldConf != nil {
+			if !protoutils.SlicesEqual(oldConf.GetRules(), config.GetPlatformComponentConfig().GetRules()) {
path: central/config/datastore/datastore.go
user: login: ajheflin
body: This commit resolves this [5c62395](https://github.com/stackrox/stackrox/pull/14984/commits/5c62395c45fb8bde20915a575f0613bcadd8d0d4)
created_at: 2025-05-14T15:19:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,6 +30,11 @@ type DataStore interface {
 	GetVulnerabilityExceptionConfig(ctx context.Context) (*storage.VulnerabilityExceptionConfig, error)
 	GetPublicConfig() (*storage.PublicConfig, error)
 	UpsertConfig(context.Context, *storage.Config) error
+
+	GetPlatformComponentConfig(context.Context) (*storage.PlatformComponentConfig, bool, error)
+	UpsertPlatformComponentConfigRule(context.Context, *storage.PlatformComponentConfig_Rule) error
+	UpsertPlatformComponentConfigRules(context.Context, []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error)
+	DeletePlatformComponentConfigRules(context.Context, ...string) error
path: central/config/datastore/datastore.go
user: login: dashrews78
body: nit:  This is probably the first one of these I've seen where the parameters are not named.  For readability I would probably name them.
created_at: 2025-05-15T14:35:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,6 +30,11 @@ type DataStore interface {
 	GetVulnerabilityExceptionConfig(ctx context.Context) (*storage.VulnerabilityExceptionConfig, error)
 	GetPublicConfig() (*storage.PublicConfig, error)
 	UpsertConfig(context.Context, *storage.Config) error
+
+	GetPlatformComponentConfig(context.Context) (*storage.PlatformComponentConfig, bool, error)
+	UpsertPlatformComponentConfigRule(context.Context, *storage.PlatformComponentConfig_Rule) error
+	UpsertPlatformComponentConfigRules(context.Context, []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error)
+	DeletePlatformComponentConfigRules(context.Context, ...string) error
path: central/config/datastore/datastore.go
user: login: dashrews78
body: Also, any reason to have `UpsertPlatformComponentConfigRule` and `UpsertPlatformComponentConfigRules` instead of just using `UpsertPlatformComponentConfigRules`?
created_at: 2025-05-15T14:38:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +247,90 @@ func clusterRetentionConfigsEqual(c1 *storage.DecommissionedClusterRetentionConf
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRule(ctx context.Context, rule *storage.PlatformComponentConfig_Rule) error {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return err
+	} else if !ok {
+		return nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return err
+	}
+	if config.PlatformComponentConfig.Rules == nil {
+		config.PlatformComponentConfig.Rules = make([]*storage.PlatformComponentConfig_Rule, 0)
+	}
+	config.PlatformComponentConfig.Rules = append(config.PlatformComponentConfig.Rules, rule)
+	return d.store.Upsert(ctx, config)
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return nil, err
+	}
+	slices.SortFunc(config.PlatformComponentConfig.Rules, ruleNameSortFunc)
+	slices.SortFunc(rules, ruleNameSortFunc)
+	if !protoutils.SlicesEqual(config.PlatformComponentConfig.Rules, rules) {
+		config.PlatformComponentConfig.NeedsReevaluation = true
+	}
path: central/config/datastore/datastore.go
user: login: dashrews78
body: Maybe we shouldn't sort these and should re-evaluate even if order changes.  Makes the code simpler by far and should not have great impact.
created_at: 2025-05-15T14:41:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +247,90 @@ func clusterRetentionConfigsEqual(c1 *storage.DecommissionedClusterRetentionConf
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRule(ctx context.Context, rule *storage.PlatformComponentConfig_Rule) error {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return err
+	} else if !ok {
+		return nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return err
+	}
+	if config.PlatformComponentConfig.Rules == nil {
+		config.PlatformComponentConfig.Rules = make([]*storage.PlatformComponentConfig_Rule, 0)
+	}
+	config.PlatformComponentConfig.Rules = append(config.PlatformComponentConfig.Rules, rule)
+	return d.store.Upsert(ctx, config)
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return nil, err
+	}
+	slices.SortFunc(config.PlatformComponentConfig.Rules, ruleNameSortFunc)
+	slices.SortFunc(rules, ruleNameSortFunc)
+	if !protoutils.SlicesEqual(config.PlatformComponentConfig.Rules, rules) {
+		config.PlatformComponentConfig.NeedsReevaluation = true
+	}
+	config.PlatformComponentConfig.Rules = rules
+	err = d.store.Upsert(ctx, config)
+	if err != nil {
+		return nil, err
+	}
+	return config.PlatformComponentConfig, nil
+}
+
+func (d *datastoreImpl) DeletePlatformComponentConfigRules(ctx context.Context, rules ...string) error {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return err
+	} else if !ok {
+		return nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return err
+	}
+	if config.PlatformComponentConfig.Rules == nil {
path: central/config/datastore/datastore.go
user: login: dashrews78
body: 1. you should use the Getters as those are nil safe.  This could throw a nil pointer if PlatformComponentConfig is nil
2. If there are no platform config rules in the existing config, how is deleting them a valid case?  Should we not  simply return an err there?
created_at: 2025-05-15T14:44:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,6 +30,11 @@ type DataStore interface {
 	GetVulnerabilityExceptionConfig(ctx context.Context) (*storage.VulnerabilityExceptionConfig, error)
 	GetPublicConfig() (*storage.PublicConfig, error)
 	UpsertConfig(context.Context, *storage.Config) error
+
+	GetPlatformComponentConfig(context.Context) (*storage.PlatformComponentConfig, bool, error)
+	UpsertPlatformComponentConfigRule(context.Context, *storage.PlatformComponentConfig_Rule) error
+	UpsertPlatformComponentConfigRules(context.Context, []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error)
+	DeletePlatformComponentConfigRules(context.Context, ...string) error
path: central/config/datastore/datastore.go
user: login: ajheflin
body: I can name them, also, the `UpsertPlatformComponentConfigRule` and `DeletePlatformComponentConfigRules` are removed in the next PR in the stack, I realized they were unnecessary. I was trying to cherry pick in the commit that deleted them but found it to be easier to not be at war with Git.
created_at: 2025-05-15T14:46:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +247,90 @@ func clusterRetentionConfigsEqual(c1 *storage.DecommissionedClusterRetentionConf
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRule(ctx context.Context, rule *storage.PlatformComponentConfig_Rule) error {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return err
+	} else if !ok {
+		return nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return err
+	}
+	if config.PlatformComponentConfig.Rules == nil {
+		config.PlatformComponentConfig.Rules = make([]*storage.PlatformComponentConfig_Rule, 0)
+	}
+	config.PlatformComponentConfig.Rules = append(config.PlatformComponentConfig.Rules, rule)
+	return d.store.Upsert(ctx, config)
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return nil, err
+	}
+	slices.SortFunc(config.PlatformComponentConfig.Rules, ruleNameSortFunc)
+	slices.SortFunc(rules, ruleNameSortFunc)
+	if !protoutils.SlicesEqual(config.PlatformComponentConfig.Rules, rules) {
+		config.PlatformComponentConfig.NeedsReevaluation = true
+	}
+	config.PlatformComponentConfig.Rules = rules
+	err = d.store.Upsert(ctx, config)
+	if err != nil {
+		return nil, err
+	}
+	return config.PlatformComponentConfig, nil
+}
+
+func (d *datastoreImpl) DeletePlatformComponentConfigRules(ctx context.Context, rules ...string) error {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return err
+	} else if !ok {
+		return nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return err
+	}
+	if config.PlatformComponentConfig.Rules == nil {
path: central/config/datastore/datastore.go
user: login: ajheflin
body: The delete method is removed in the next PR so this will be gone once that's merged into this PR (the commit has now been moved up to this PR)
created_at: 2025-05-15T14:46:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +247,90 @@ func clusterRetentionConfigsEqual(c1 *storage.DecommissionedClusterRetentionConf
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRule(ctx context.Context, rule *storage.PlatformComponentConfig_Rule) error {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return err
+	} else if !ok {
+		return nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return err
+	}
+	if config.PlatformComponentConfig.Rules == nil {
+		config.PlatformComponentConfig.Rules = make([]*storage.PlatformComponentConfig_Rule, 0)
+	}
+	config.PlatformComponentConfig.Rules = append(config.PlatformComponentConfig.Rules, rule)
+	return d.store.Upsert(ctx, config)
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return nil, err
+	}
+	slices.SortFunc(config.PlatformComponentConfig.Rules, ruleNameSortFunc)
+	slices.SortFunc(rules, ruleNameSortFunc)
+	if !protoutils.SlicesEqual(config.PlatformComponentConfig.Rules, rules) {
+		config.PlatformComponentConfig.NeedsReevaluation = true
+	}
path: central/config/datastore/datastore.go
user: login: ajheflin
body: I can do that, but should we be concerned about the reprocessing compute usage for large environments with many deployments/alerts?
created_at: 2025-05-15T14:47:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +247,90 @@ func clusterRetentionConfigsEqual(c1 *storage.DecommissionedClusterRetentionConf
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRule(ctx context.Context, rule *storage.PlatformComponentConfig_Rule) error {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return err
+	} else if !ok {
+		return nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return err
+	}
+	if config.PlatformComponentConfig.Rules == nil {
+		config.PlatformComponentConfig.Rules = make([]*storage.PlatformComponentConfig_Rule, 0)
+	}
+	config.PlatformComponentConfig.Rules = append(config.PlatformComponentConfig.Rules, rule)
+	return d.store.Upsert(ctx, config)
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return nil, err
+	}
+	slices.SortFunc(config.PlatformComponentConfig.Rules, ruleNameSortFunc)
+	slices.SortFunc(rules, ruleNameSortFunc)
+	if !protoutils.SlicesEqual(config.PlatformComponentConfig.Rules, rules) {
+		config.PlatformComponentConfig.NeedsReevaluation = true
+	}
path: central/config/datastore/datastore.go
user: login: charmik-redhat
body: That's a good point. A non-update can trigger a large scale reprocessing. For example if someone just changes the image retention config, but the UI sends the platform config rules in a different order in PUT v1/config request, that'll trigger a reprocessing.
created_at: 2025-05-16T13:53:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +242,39 @@ func clusterRetentionConfigsEqual(c1 *storage.DecommissionedClusterRetentionConf
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, nil
path: central/config/datastore/datastore.go
user: login: charmik-redhat
body: That should probably return `sac.ErrResourceAccessDenied` error
created_at: 2025-05-16T13:58:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +242,39 @@ func clusterRetentionConfigsEqual(c1 *storage.DecommissionedClusterRetentionConf
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
path: central/config/datastore/datastore.go
user: login: charmik-redhat
body: Should it return an `errox.NotFound` error when config is not found?
created_at: 2025-05-16T14:02:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +242,39 @@ func clusterRetentionConfigsEqual(c1 *storage.DecommissionedClusterRetentionConf
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return nil, err
+	}
+	if !protoutils.SlicesEqual(config.PlatformComponentConfig.Rules, rules) {
path: central/config/datastore/datastore.go
user: login: charmik-redhat
body: `config.GetPlatformComponentConfig().GetRules()` if the old platform config is nil for some unintended reason. Also if `config.GetPlatformConfig()` is nil, you can initialize it here.
created_at: 2025-05-16T14:06:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +242,39 @@ func clusterRetentionConfigsEqual(c1 *storage.DecommissionedClusterRetentionConf
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found || err != nil {
+		return nil, err
+	}
+	if !protoutils.SlicesEqual(config.PlatformComponentConfig.Rules, rules) {
+		config.PlatformComponentConfig.NeedsReevaluation = true
+	}
+	config.PlatformComponentConfig.Rules = rules
+	err = d.store.Upsert(ctx, config)
+	if err != nil {
+		return nil, err
+	}
+	return config.PlatformComponentConfig, nil
path: central/config/datastore/datastore.go
user: login: charmik-redhat
body: nit return config.GetPlatformConfig()
created_at: 2025-05-16T14:09:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -194,6 +198,19 @@ func (d *datastoreImpl) UpsertConfig(ctx context.Context, config *storage.Config
 			clusterRetentionConf.LastUpdated = protocompat.TimestampNow()
 		}
 	}
+	if platformComponentConfig := config.GetPlatformComponentConfig(); platformComponentConfig != nil {
+		oldConf, _, err := d.GetPlatformComponentConfig(ctx)
+		if err != nil {
+			return err
+		}
+		if oldConf != nil {
+			if !protoutils.SlicesEqual(oldConf.GetRules(), config.GetPlatformComponentConfig().GetRules()) {
+				config.PlatformComponentConfig.NeedsReevaluation = true
path: central/config/datastore/datastore.go
user: login: charmik-redhat
body: You might also want to validate these 
1) regexes are valid 
2) The names of rules are unique
3) The "system rule" (`PlatformComponentSystemRuleName`) exists and its value is the same as `PlatformComponentSystemRegex` 

For regex validation, you do something like this
https://github.com/stackrox/stackrox/blob/master/central/resourcecollection/datastore/datastore_impl.go#L607

created_at: 2025-05-16T14:25:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -194,6 +198,19 @@ func (d *datastoreImpl) UpsertConfig(ctx context.Context, config *storage.Config
 			clusterRetentionConf.LastUpdated = protocompat.TimestampNow()
 		}
 	}
+	if platformComponentConfig := config.GetPlatformComponentConfig(); platformComponentConfig != nil {
+		oldConf, _, err := d.GetPlatformComponentConfig(ctx)
+		if err != nil {
+			return err
+		}
+		if oldConf != nil {
+			if !protoutils.SlicesEqual(oldConf.GetRules(), config.GetPlatformComponentConfig().GetRules()) {
+				config.PlatformComponentConfig.NeedsReevaluation = true
path: central/config/datastore/datastore.go
user: login: ajheflin
body: The compilation is done [here](https://github.com/stackrox/stackrox/blob/9f827ad31900525d657c6952abbbef931b83d7ae/central/config/service/service.go#L228-L238). It had to be a part of the other PR since the other PR creates the place where the regexes are actually used, and this PR would fail to compile if I had the unused variable.

As for the other two notes, I was planning to add some guarding for those other two rules, I've just been battling with CI on the other PR and was planning to get to that as soon as the other PR passes CI.
created_at: 2025-05-16T14:47:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +239,84 @@ func clusterRetentionConfigsEqual(c1 *storage.DecommissionedClusterRetentionConf
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, sac.ErrResourceAccessDenied
+	}
+
+	config, found, err := d.store.Get(ctx)
path: central/config/datastore/datastore.go
user: login: charmik-redhat
body: No need to initialize the whole config here. You can just return an error if the entire config doesn't exist in the db.
created_at: 2025-05-20T23:31:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -194,6 +201,13 @@ func (d *datastoreImpl) UpsertConfig(ctx context.Context, config *storage.Config
 			clusterRetentionConf.LastUpdated = protocompat.TimestampNow()
 		}
 	}
+	if platformComponentConfig := config.GetPlatformComponentConfig(); platformComponentConfig != nil {
+		platformConfig, err := d.UpsertPlatformComponentConfigRules(ctx, config.GetPlatformComponentConfig().GetRules())
path: central/config/datastore/datastore.go
user: login: charmik-redhat
body: I think this would lead to calling `store.upsert(...)` twice - Once inside  `UpsertPlatformComponentConfigRules(...)` and then again on line 212. Maybe you can just move the validations out of `UpsertPlatformComponentConfigRules` into a common function and call that from here and also from `UpsertPlatformComponentConfigRules`
created_at: 2025-05-20T23:34:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +239,84 @@ func clusterRetentionConfigsEqual(c1 *storage.DecommissionedClusterRetentionConf
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, sac.ErrResourceAccessDenied
+	}
+
+	config, found, err := d.store.Get(ctx)
path: central/config/datastore/datastore.go
user: login: ajheflin
body: I can't, because of this stack of calls:
- https://github.com/stackrox/stackrox/blob/86947b72195fe29a819bd4113d79fb9e93a686b0/central/config/datastore/singleton.go#L182
  - https://github.com/stackrox/stackrox/blob/86947b72195fe29a819bd4113d79fb9e93a686b0/central/config/datastore/datastore.go#L205

If this method returns an error, `UpsertConfig` returns an error, and then the `.must()` call in the config singleton initialization fails, and thus Central fails to start. I could modify it returning the nested error in `UpsertConfig`, but I feel like that would lead to potential issues down the line where it's silently failing to update the platform component config when trying to update the config via that method
created_at: 2025-05-21T13:53:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -194,6 +201,13 @@ func (d *datastoreImpl) UpsertConfig(ctx context.Context, config *storage.Config
 			clusterRetentionConf.LastUpdated = protocompat.TimestampNow()
 		}
 	}
+	if platformComponentConfig := config.GetPlatformComponentConfig(); platformComponentConfig != nil {
+		platformConfig, err := d.UpsertPlatformComponentConfigRules(ctx, config.GetPlatformComponentConfig().GetRules())
path: central/config/datastore/datastore.go
user: login: ajheflin
body: I didn't see this comment until after writing my previous comment, good call. Let me see what I can do...
created_at: 2025-05-21T13:54:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -194,6 +202,14 @@ func (d *datastoreImpl) UpsertConfig(ctx context.Context, config *storage.Config
 			clusterRetentionConf.LastUpdated = protocompat.TimestampNow()
 		}
 	}
+	if platformComponentConfig := config.GetPlatformComponentConfig(); platformComponentConfig != nil {
path: central/config/datastore/datastore.go
user: login: charmik-redhat
body: nit `if config.GetPlatformComponentConfig() != nil {`
created_at: 2025-05-21T21:01:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,3 +241,93 @@ func clusterRetentionConfigsEqual(c1 *storage.DecommissionedClusterRetentionConf
 	return c1.GetRetentionDurationDays() == c2.GetRetentionDurationDays() &&
 		reflect.DeepEqual(c1.GetIgnoreClusterLabels(), c2.GetIgnoreClusterLabels())
 }
+
+func (d *datastoreImpl) GetPlatformComponentConfig(ctx context.Context) (*storage.PlatformComponentConfig, bool, error) {
+	if ok, err := administrationSAC.ReadAllowed(ctx); err != nil {
+		return nil, false, err
+	} else if !ok {
+		return nil, false, nil
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if config == nil {
+		return nil, false, nil
+	}
+	return config.GetPlatformComponentConfig(), found && config.GetPlatformComponentConfig() != nil, err
+}
+
+func (d *datastoreImpl) UpsertPlatformComponentConfigRules(ctx context.Context, rules []*storage.PlatformComponentConfig_Rule) (*storage.PlatformComponentConfig, error) {
+	if ok, err := administrationSAC.WriteAllowed(ctx); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, sac.ErrResourceAccessDenied
+	}
+
+	config, found, err := d.store.Get(ctx)
+	if !found {
+		return nil, errox.NotFound
path: central/config/datastore/datastore.go
user: login: charmik-redhat
body: nit `return errors.Wrap(errox.NotFound, "System configuration not found")`
created_at: 2025-05-21T21:09:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: openshift-ci[bot]
created_at: 2025-04-14T19:32:52Z
body: Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with `/test all`
author_association: NONE
user: login: rhacs-bot
created_at: 2025-04-14T21:03:54Z
body: [//]: # (meta:type=build)

Images are ready for the commit at cdf9071.

To use with deploy scripts, first `export MAIN_IMAGE_TAG=4.8.x-736-gcdf9071620`.
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-14T21:41:24Z
body: ## [Codecov](https://app.codecov.io/gh/stackrox/stackrox/pull/14984?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) Report
Attention: Patch coverage is `40.20619%` with `58 lines` in your changes missing coverage. Please review.
> Project coverage is 49.23%. Comparing base [(`6a8dbeb`)](https://app.codecov.io/gh/stackrox/stackrox/commit/6a8dbebd2fb0bae52db4502b64558b3ddb10aa4a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) to head [(`cdf9071`)](https://app.codecov.io/gh/stackrox/stackrox/commit/cdf907162074d2a73ecc7048e3d0c95943c945fa?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox).
> Report is 80 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/stackrox/stackrox/pull/14984?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) | Patch % | Lines |
|---|---|---|
| [central/config/datastore/datastore.go](https://app.codecov.io/gh/stackrox/stackrox/pull/14984?src=pr&el=tree&filepath=central%2Fconfig%2Fdatastore%2Fdatastore.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y2VudHJhbC9jb25maWcvZGF0YXN0b3JlL2RhdGFzdG9yZS5nbw==) | 49.36% | [27 Missing and 13 partials :warning: ](https://app.codecov.io/gh/stackrox/stackrox/pull/14984?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [central/config/service/service.go](https://app.codecov.io/gh/stackrox/stackrox/pull/14984?src=pr&el=tree&filepath=central%2Fconfig%2Fservice%2Fservice.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y2VudHJhbC9jb25maWcvc2VydmljZS9zZXJ2aWNlLmdv) | 0.00% | [18 Missing :warning: ](https://app.codecov.io/gh/stackrox/stackrox/pull/14984?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master   #14984      +/-   ##
==========================================
+ Coverage   49.09%   49.23%   +0.13%     
==========================================
  Files        2568     2573       +5     
  Lines      188401   188942     +541     
==========================================
+ Hits        92502    93030     +528     
+ Misses      88601    88581      -20     
- Partials     7298     7331      +33     
```

| [Flag](https://app.codecov.io/gh/stackrox/stackrox/pull/14984/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) | Coverage Δ | |
|---|---|---|
| [go-unit-tests](https://app.codecov.io/gh/stackrox/stackrox/pull/14984/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) | `49.23% <40.20%> (+0.13%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/stackrox/stackrox/pull/14984?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: ajheflin
created_at: 2025-05-15T19:41:09Z
body: /retest
author_association: CONTRIBUTOR