From cb5218fe1da0f3a75f950609439fc416e566144a Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 1 Feb 2026 19:01:29 +0200 Subject: [PATCH 1/4] Implement ClusterUID enrichment for runtime alerts This change adds support for enriching runtime alerts with a stable cluster identifier (ClusterUID) based on the kube-system namespace UID. Changes: - Update armoapi-go dependency to v0.0.672 (includes ClusterUID field) - Add GetClusterUID utility function to fetch kube-system namespace UID - Update HTTPExporter to accept and store clusterUID - Update InitExporters to accept clusterUID parameter - Populate ClusterUID in createRuleAlert and createMalwareAlert - Fetch ClusterUID at agent startup in main.go The ClusterUID is fetched once at startup and passed to all exporters. If the kube-system namespace cannot be accessed (e.g., RBAC restrictions), a warning is logged and the field remains empty, allowing the agent to continue operating normally. Related PR: armosec/armoapi-go#602 Signed-off-by: Ben --- cmd/main.go | 10 +++++++--- go.mod | 2 +- go.sum | 4 ++-- pkg/exporters/exporters_bus.go | 4 ++-- pkg/exporters/http_exporter.go | 7 ++++++- pkg/utils/clusteruid.go | 25 +++++++++++++++++++++++++ 6 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 pkg/utils/clusteruid.go diff --git a/cmd/main.go b/cmd/main.go index 635101cc8..b34185e46 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -162,6 +162,10 @@ func main() { // Create clients logger.L().Info("Kubernetes mode is true") k8sClient := k8sinterface.NewKubernetesApi() + + // Fetch cluster UID from kube-system namespace + clusterUID := utils.GetClusterUID(k8sClient.GetKubernetesClient()) + storageClient, err := storage.CreateStorage(clusterData.Namespace) if err != nil { logger.L().Ctx(ctx).Fatal("error creating the storage client", helpers.Error(err)) @@ -282,7 +286,7 @@ func main() { if cfg.EnableRuntimeDetection { // create exporter - exporter := exporters.InitExporters(cfg.Exporters, clusterData.ClusterName, cfg.NodeName, cloudMetadata) + exporter := exporters.InitExporters(cfg.Exporters, clusterData.ClusterName, cfg.NodeName, cloudMetadata, clusterUID) dWatcher.AddAdaptor(ruleBindingCache) ruleBindingNotify = make(chan rulebinding.RuleBindingNotify, 100) @@ -348,7 +352,7 @@ func main() { var malwareManager malwaremanager.MalwareManagerClient if cfg.EnableMalwareDetection { // create exporter - exporter := exporters.InitExporters(cfg.Exporters, clusterData.ClusterName, cfg.NodeName, cloudMetadata) + exporter := exporters.InitExporters(cfg.Exporters, clusterData.ClusterName, cfg.NodeName, cloudMetadata, clusterUID) malwareManager, err = malwaremanagerv1.CreateMalwareManager(cfg, k8sClient, cfg.NodeName, clusterData.ClusterName, exporter, prometheusExporter, k8sObjectCache) if err != nil { logger.L().Ctx(ctx).Fatal("error creating MalwareManager", helpers.Error(err)) @@ -385,7 +389,7 @@ func main() { if cfg.EnableFIM { // Initialize FIM-specific exporters fimExportersConfig := cfg.FIM.GetFIMExportersConfig() - fimExporter := exporters.InitExporters(fimExportersConfig, clusterData.ClusterName, cfg.NodeName, cloudMetadata) + fimExporter := exporters.InitExporters(fimExportersConfig, clusterData.ClusterName, cfg.NodeName, cloudMetadata, clusterUID) fimManager, err = fimmanager.NewFIMManager(cfg, clusterData.ClusterName, fimExporter, cloudMetadata) if err != nil { diff --git a/go.mod b/go.mod index b5f6a6427..728c3d38f 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/anchore/syft v1.32.0 github.com/aquilax/truncate v1.0.0 - github.com/armosec/armoapi-go v0.0.671 + github.com/armosec/armoapi-go v0.0.672 github.com/armosec/utils-k8s-go v0.0.35 github.com/cenkalti/backoff v2.2.1+incompatible github.com/cenkalti/backoff/v4 v4.3.0 diff --git a/go.sum b/go.sum index 087234eab..b34367d2c 100644 --- a/go.sum +++ b/go.sum @@ -761,8 +761,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/armosec/armoapi-go v0.0.671 h1:amHT4tTa1h/z4hf/F/DO5VkdrEtvckOpwwMos36LT3o= -github.com/armosec/armoapi-go v0.0.671/go.mod h1:9jAH0g8ZsryhiBDd/aNMX4+n10bGwTx/doWCyyjSxts= +github.com/armosec/armoapi-go v0.0.672 h1:Js3yvV3GnqYCw3Dyq5HHo9br1mCthgrVwHuWCzNX/2w= +github.com/armosec/armoapi-go v0.0.672/go.mod h1:9jAH0g8ZsryhiBDd/aNMX4+n10bGwTx/doWCyyjSxts= github.com/armosec/gojay v1.2.17 h1:VSkLBQzD1c2V+FMtlGFKqWXNsdNvIKygTKJI9ysY8eM= github.com/armosec/gojay v1.2.17/go.mod h1:vuvX3DlY0nbVrJ0qCklSS733AWMoQboq3cFyuQW9ybc= github.com/armosec/utils-go v0.0.58 h1:g9RnRkxZAmzTfPe2ruMo2OXSYLwVSegQSkSavOfmaIE= diff --git a/pkg/exporters/exporters_bus.go b/pkg/exporters/exporters_bus.go index 2c2cf611c..b969dc3e3 100644 --- a/pkg/exporters/exporters_bus.go +++ b/pkg/exporters/exporters_bus.go @@ -29,7 +29,7 @@ type ExporterBus struct { } // InitExporters initializes all exporters. -func InitExporters(exportersConfig ExportersConfig, clusterName string, nodeName string, cloudMetadata *armotypes.CloudMetadata) *ExporterBus { +func InitExporters(exportersConfig ExportersConfig, clusterName string, nodeName string, cloudMetadata *armotypes.CloudMetadata, clusterUID string) *ExporterBus { var exporters []Exporter for _, url := range exportersConfig.AlertManagerExporterUrls { alertMan := InitAlertManagerExporter(url) @@ -56,7 +56,7 @@ func InitExporters(exportersConfig ExportersConfig, clusterName string, nodeName } } if exportersConfig.HTTPExporterConfig != nil { - httpExporter, err := NewHTTPExporter(*exportersConfig.HTTPExporterConfig, clusterName, nodeName, cloudMetadata) + httpExporter, err := NewHTTPExporter(*exportersConfig.HTTPExporterConfig, clusterName, nodeName, cloudMetadata, clusterUID) if err == nil { exporters = append(exporters, httpExporter) } else { diff --git a/pkg/exporters/http_exporter.go b/pkg/exporters/http_exporter.go index 05625d8e4..e83491a22 100644 --- a/pkg/exporters/http_exporter.go +++ b/pkg/exporters/http_exporter.go @@ -67,6 +67,7 @@ type HTTPExporter struct { host string nodeName string clusterName string + clusterUID string httpClient *http.Client alertMetrics *alertMetrics cloudMetadata *apitypes.CloudMetadata @@ -94,7 +95,7 @@ type HTTPAlertsListSpec struct { } // NewHTTPExporter creates a new HTTPExporter instance -func NewHTTPExporter(config HTTPExporterConfig, clusterName, nodeName string, cloudMetadata *apitypes.CloudMetadata) (*HTTPExporter, error) { +func NewHTTPExporter(config HTTPExporterConfig, clusterName, nodeName string, cloudMetadata *apitypes.CloudMetadata, clusterUID string) (*HTTPExporter, error) { if err := config.Validate(); err != nil { return nil, fmt.Errorf("invalid config: %w", err) } @@ -103,6 +104,7 @@ func NewHTTPExporter(config HTTPExporterConfig, clusterName, nodeName string, cl config: config, nodeName: nodeName, clusterName: clusterName, + clusterUID: clusterUID, httpClient: &http.Client{ Timeout: time.Duration(config.TimeoutSeconds) * time.Second, }, @@ -315,9 +317,11 @@ func (e *HTTPExporter) createRuleAlert(failedRule types.RuleFailure) apitypes.Ru k8sDetails := failedRule.GetRuntimeAlertK8sDetails() k8sDetails.NodeName = e.nodeName k8sDetails.ClusterName = e.clusterName + k8sDetails.ClusterUID = e.clusterUID httpDetails := failedRule.GetHttpRuleAlert() httpDetails.SourcePodInfo.ClusterName = e.clusterName + httpDetails.SourcePodInfo.ClusterUID = e.clusterUID return apitypes.RuntimeAlert{ Message: failedRule.GetRuleAlert().RuleDescription, @@ -336,6 +340,7 @@ func (e *HTTPExporter) createMalwareAlert(result malwaremanager.MalwareResult) a k8sDetails := result.GetRuntimeAlertK8sDetails() k8sDetails.NodeName = e.nodeName k8sDetails.ClusterName = e.clusterName + k8sDetails.ClusterUID = e.clusterUID return apitypes.RuntimeAlert{ Message: fmt.Sprintf("Malware detected: %s", result.GetBasicRuntimeAlert().AlertName), diff --git a/pkg/utils/clusteruid.go b/pkg/utils/clusteruid.go new file mode 100644 index 000000000..b97f9044b --- /dev/null +++ b/pkg/utils/clusteruid.go @@ -0,0 +1,25 @@ +package utils + +import ( + "context" + + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// GetClusterUID retrieves the UID of the kube-system namespace to use as a stable cluster identifier. +// If the namespace cannot be accessed (e.g., due to RBAC restrictions), it returns an empty string and logs a warning. +func GetClusterUID(k8sClient kubernetes.Interface) string { + ctx := context.Background() + namespace, err := k8sClient.CoreV1().Namespaces().Get(ctx, "kube-system", metav1.GetOptions{}) + if err != nil { + logger.L().Ctx(ctx).Warning("failed to get kube-system namespace for ClusterUID", helpers.Error(err)) + return "" + } + + clusterUID := string(namespace.UID) + logger.L().Ctx(ctx).Info("successfully retrieved ClusterUID", helpers.String("clusterUID", clusterUID)) + return clusterUID +} From 3308069d0d82f3ae46119c07e663dd682c13caa6 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 1 Feb 2026 19:20:13 +0200 Subject: [PATCH 2/4] Fix test calls to NewHTTPExporter with new clusterUID parameter Signed-off-by: Ben --- pkg/exporters/http_exporter_test.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/exporters/http_exporter_test.go b/pkg/exporters/http_exporter_test.go index 42cc69053..8b52a0bba 100644 --- a/pkg/exporters/http_exporter_test.go +++ b/pkg/exporters/http_exporter_test.go @@ -32,7 +32,7 @@ func TestSendRuleAlert(t *testing.T) { // Create an HTTPExporter with the mock server URL exporter, err := NewHTTPExporter(HTTPExporterConfig{ URL: server.URL, - }, "", "", nil) + }, "", "", nil, "") assert.NoError(t, err) // Create a mock rule failure @@ -93,7 +93,7 @@ func TestSendRuleAlertRateReached(t *testing.T) { exporter, err := NewHTTPExporter(HTTPExporterConfig{ URL: server.URL, MaxAlertsPerMinute: 1, - }, "", "", nil) + }, "", "", nil, "") assert.NoError(t, err) // Create a mock rule failure @@ -159,7 +159,7 @@ func TestSendMalwareAlertHTTPExporter(t *testing.T) { // Create an HTTPExporter with the mock server URL exporter, err := NewHTTPExporter(HTTPExporterConfig{ URL: server.URL, - }, "", "", nil) + }, "", "", nil, "") assert.NoError(t, err) // Create a mock malware description @@ -213,13 +213,13 @@ func TestSendMalwareAlertHTTPExporter(t *testing.T) { func TestValidateHTTPExporterConfig(t *testing.T) { // Test case: URL is empty - _, err := NewHTTPExporter(HTTPExporterConfig{}, "", "", nil) + _, err := NewHTTPExporter(HTTPExporterConfig{}, "", "", nil, "") assert.Error(t, err) // Test case: URL is not empty exp, err := NewHTTPExporter(HTTPExporterConfig{ URL: "http://localhost:9093", - }, "cluster", "node", nil) + }, "cluster", "node", nil, "test-cluster-uid") assert.NoError(t, err) assert.Equal(t, "POST", exp.config.Method) assert.Equal(t, 5, exp.config.TimeoutSeconds) @@ -227,6 +227,7 @@ func TestValidateHTTPExporterConfig(t *testing.T) { assert.Equal(t, []HTTPKeyValues{}, exp.config.Headers) assert.Equal(t, "cluster", exp.clusterName) assert.Equal(t, "node", exp.nodeName) + assert.Equal(t, "test-cluster-uid", exp.clusterUID) // Test case: Method is PUT exp, err = NewHTTPExporter(HTTPExporterConfig{ @@ -240,7 +241,7 @@ func TestValidateHTTPExporterConfig(t *testing.T) { Value: "Bearer token", }, }, - }, "", "", nil) + }, "", "", nil, "") assert.NoError(t, err) assert.Equal(t, "PUT", exp.config.Method) assert.Equal(t, 2, exp.config.TimeoutSeconds) @@ -251,6 +252,6 @@ func TestValidateHTTPExporterConfig(t *testing.T) { _, err = NewHTTPExporter(HTTPExporterConfig{ URL: "http://localhost:9093", Method: "DELETE", - }, "", "", nil) + }, "", "", nil, "") assert.Error(t, err) } From d087ad5e03cbc07e87b89a7e3126c1e174b9467b Mon Sep 17 00:00:00 2001 From: Ben Hirschberg <59160382+slashben@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:49:14 +0200 Subject: [PATCH 3/4] Update pkg/utils/clusteruid.go Co-authored-by: Matthias Bertschy Signed-off-by: Ben Hirschberg <59160382+slashben@users.noreply.github.com> --- pkg/utils/clusteruid.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/utils/clusteruid.go b/pkg/utils/clusteruid.go index b97f9044b..60c6bbc72 100644 --- a/pkg/utils/clusteruid.go +++ b/pkg/utils/clusteruid.go @@ -12,7 +12,8 @@ import ( // GetClusterUID retrieves the UID of the kube-system namespace to use as a stable cluster identifier. // If the namespace cannot be accessed (e.g., due to RBAC restrictions), it returns an empty string and logs a warning. func GetClusterUID(k8sClient kubernetes.Interface) string { - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() namespace, err := k8sClient.CoreV1().Namespaces().Get(ctx, "kube-system", metav1.GetOptions{}) if err != nil { logger.L().Ctx(ctx).Warning("failed to get kube-system namespace for ClusterUID", helpers.Error(err)) From 3bd0607f8031d4e65c00d23dfa1a226343a5f782 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 2 Feb 2026 15:14:23 +0100 Subject: [PATCH 4/4] add missing import to clusteruid.go Signed-off-by: Matthias Bertschy --- pkg/utils/clusteruid.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/utils/clusteruid.go b/pkg/utils/clusteruid.go index 60c6bbc72..fa19748a9 100644 --- a/pkg/utils/clusteruid.go +++ b/pkg/utils/clusteruid.go @@ -2,6 +2,7 @@ package utils import ( "context" + "time" "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" @@ -12,14 +13,14 @@ import ( // GetClusterUID retrieves the UID of the kube-system namespace to use as a stable cluster identifier. // If the namespace cannot be accessed (e.g., due to RBAC restrictions), it returns an empty string and logs a warning. func GetClusterUID(k8sClient kubernetes.Interface) string { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() namespace, err := k8sClient.CoreV1().Namespaces().Get(ctx, "kube-system", metav1.GetOptions{}) if err != nil { logger.L().Ctx(ctx).Warning("failed to get kube-system namespace for ClusterUID", helpers.Error(err)) return "" } - + clusterUID := string(namespace.UID) logger.L().Ctx(ctx).Info("successfully retrieved ClusterUID", helpers.String("clusterUID", clusterUID)) return clusterUID