From e953a9e541419443df21bd1582235aa8fb53a562 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Fri, 12 Jun 2026 15:10:08 +0200 Subject: [PATCH 1/3] chore(deps): bump armoapi-go to v0.0.719 Modernize the operator's armoapi-go from v0.0.673 to v0.0.719 to pick up the rule open-protection types (armotypes.OpenMatchers / UnionOpenProtection, ProfileDataRequired schema). This unblocks a follow-up watcher that resolves RuntimeRuleAlertBinding selectors against the rule library and publishes the union of profileDataRequired.opens for the storage apiserver. The bump is additive: node-agent (v0.3.38) and storage (v0.0.239) compile unchanged against the newer armoapi, so no coordinated bump is required here. go build, go vet, and the watcher test suite all pass. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Matthias Bertschy --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 82244f8..532a2d8 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/kubescape/operator go 1.25.8 require ( - github.com/armosec/armoapi-go v0.0.673 + github.com/armosec/armoapi-go v0.0.719 github.com/armosec/registryx v0.0.35 github.com/armosec/utils-go v0.0.58 github.com/armosec/utils-k8s-go v0.0.35 diff --git a/go.sum b/go.sum index 1d04895..07b5144 100644 --- a/go.sum +++ b/go.sum @@ -145,8 +145,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armosec/armoapi-go v0.0.673 h1:Pwsf/K2Y1U8IExntU0iD+F0iAz1ys6BhiPyqEu2hw+Y= -github.com/armosec/armoapi-go v0.0.673/go.mod h1:9jAH0g8ZsryhiBDd/aNMX4+n10bGwTx/doWCyyjSxts= +github.com/armosec/armoapi-go v0.0.719 h1:eo35KTOPS0vM3asLfDONwNScZ+1FRhUprpNWs2czXCM= +github.com/armosec/armoapi-go v0.0.719/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/registryx v0.0.35 h1:fvP5/IZL0+jJUnNXvAj6ohJ6UVHaASVpR/cPLX0I4f0= From 68990cc0de70dd120acff4cd59784af3804cd45e Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 15 Jun 2026 07:16:23 +0200 Subject: [PATCH 2/3] chore(deps): pull in rulelibrary + bump node-agent/storage for open-protection Add github.com/armosec/rulelibrary (open-protection union helper) and let MVS pull its direct requirements forward: - node-agent v0.3.38 -> v0.3.98-... - storage v0.0.239 -> v0.0.268 The only API drift is a constant rename in k8s-interface's instanceidhandler helpers (same label strings): {Kind,Name,Namespace}MetadataKey -> Related{Kind,Name,Namespace}MetadataKey. Updated containerprofilewatcher and its test accordingly. This is the dependency groundwork for the open-protection watcher (next commit), which resolves RuntimeRuleAlertBinding selectors against the rule library and publishes the union of profileDataRequired.opens as a ConfigMap. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Matthias Bertschy --- go.mod | 48 ++++++------- go.sum | 96 ++++++++++++------------- watcher/containerprofilewatcher.go | 12 ++-- watcher/containerprofilewatcher_test.go | 26 +++---- 4 files changed, 91 insertions(+), 91 deletions(-) diff --git a/go.mod b/go.mod index 532a2d8..f1ebb1a 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,9 @@ require ( github.com/armosec/registryx v0.0.35 github.com/armosec/utils-go v0.0.58 github.com/armosec/utils-k8s-go v0.0.35 - github.com/aws/aws-sdk-go-v2 v1.41.2 - github.com/aws/aws-sdk-go-v2/config v1.32.10 - github.com/aws/aws-sdk-go-v2/credentials v1.19.10 + github.com/aws/aws-sdk-go-v2 v1.41.5 + github.com/aws/aws-sdk-go-v2/config v1.32.12 + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 github.com/aws/aws-sdk-go-v2/service/ecr v1.55.3 github.com/cenkalti/backoff v2.2.1+incompatible github.com/cenkalti/backoff/v4 v4.3.0 @@ -21,13 +21,13 @@ require ( github.com/google/uuid v1.6.0 github.com/goradd/maps v1.3.0 github.com/gorilla/mux v1.8.1 - github.com/kubescape/backend v0.0.37 + github.com/kubescape/backend v0.0.40 github.com/kubescape/go-logger v0.0.28 - github.com/kubescape/k8s-interface v0.0.202 + github.com/kubescape/k8s-interface v0.0.206 github.com/kubescape/kubescape-network-scanner v0.0.15 - github.com/kubescape/node-agent v0.3.38 + github.com/kubescape/node-agent v0.3.98-0.20260423123245-49b120190105 github.com/kubescape/opa-utils v0.0.285 - github.com/kubescape/storage v0.0.239 + github.com/kubescape/storage v0.0.268 github.com/panjf2000/ants/v2 v2.11.3 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 @@ -47,7 +47,7 @@ require ( ) require ( - cloud.google.com/go/auth v0.18.1 // indirect + cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/container v1.45.0 // indirect @@ -110,20 +110,20 @@ require ( github.com/armosec/gojay v1.2.17 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0 // indirect github.com/aws/aws-sdk-go-v2/service/eks v1.48.5 // indirect github.com/aws/aws-sdk-go-v2/service/iam v1.35.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect - github.com/aws/smithy-go v1.24.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect + github.com/aws/smithy-go v1.24.2 // indirect github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect @@ -196,7 +196,7 @@ require ( github.com/google/go-containerregistry v0.21.2 // indirect github.com/google/licensecheck v0.3.1 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect @@ -209,7 +209,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.4 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/kubescape/kubescape/v3 v3.0.4 // indirect github.com/kubescape/rbac-utils v0.0.21-0.20230806101615-07e36f555520 // indirect github.com/kubescape/regolibrary v1.0.317-0.20240320124840-1d84ac7186ea // indirect @@ -249,7 +249,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 // indirect - github.com/picatz/xcel v0.0.0-20250816143731-885b5f678a12 // indirect + github.com/picatz/xcel v0.0.0-20260226001349-6958ffac5706 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect @@ -298,7 +298,7 @@ require ( github.com/yashtewari/glob-intersection v0.2.0 // indirect github.com/yl2chen/cidranger v1.0.2 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.mongodb.org/mongo-driver v1.17.4 // indirect + go.mongodb.org/mongo-driver v1.17.6 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/otelslog v0.18.0 // indirect @@ -332,7 +332,7 @@ require ( golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect gonum.org/v1/gonum v0.17.0 // indirect - google.golang.org/api v0.267.0 // indirect + google.golang.org/api v0.271.0 // indirect google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect diff --git a/go.sum b/go.sum index 07b5144..4a42f63 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0c cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= -cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -159,20 +159,20 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= -github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= -github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= -github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= -github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= +github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/service/ecr v1.55.3 h1:RtGctYMmkTerGClvdY6bHXdtly4FeYw9wz/NPz62LF8= github.com/aws/aws-sdk-go-v2/service/ecr v1.55.3/go.mod h1:vBfBu24Ka3/5UZtepbTV0gnc9VPLT8ok+0oDDaYAzn4= github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0 h1:MzP/ElwTpINq+hS80ZQz4epKVnUTlz8Sz+P/AFORCKM= @@ -181,20 +181,20 @@ github.com/aws/aws-sdk-go-v2/service/eks v1.48.5 h1:vMwwdzKoUBt7vMHNkF16Hh7+8ndV github.com/aws/aws-sdk-go-v2/service/eks v1.48.5/go.mod h1:9dn8p15siUL80NCTPVNd+YvEpVTmWO+rboGx6qOMBa0= github.com/aws/aws-sdk-go-v2/service/iam v1.35.3 h1:bWFkGGea2UoD/m229uuRfT0mu+6pKNB0Kq4U6j/Qz3U= github.com/aws/aws-sdk-go-v2/service/iam v1.35.3/go.mod h1:PpmEOH3ZTQlDAezieBVdFMjPO1jovUMNPA4OpCtnwbY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= -github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0= -github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -571,8 +571,8 @@ github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= -github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -684,8 +684,8 @@ github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -698,26 +698,26 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kubescape/backend v0.0.37 h1:aAMd5M0Ih4h+enD0LdKzVIDXYVFqEuFBkSyjiGtocnc= -github.com/kubescape/backend v0.0.37/go.mod h1:4TjTNf9GSD2XxrnW6doB3ANSFzFEkXKYZHGFQX0BiKM= +github.com/kubescape/backend v0.0.40 h1:nwQ74sbtuvwkzewS3axf6pIA6C6v+X4e4AzLuvVvoxI= +github.com/kubescape/backend v0.0.40/go.mod h1:cMEGP8cXUZgY89YU4GRBGIla9HZW7grZsUtlCwvZgAE= github.com/kubescape/go-logger v0.0.28 h1:xulKTp9kOg3rD98sopFELQ6yZCHQoQXMDzteoSHDFKI= github.com/kubescape/go-logger v0.0.28/go.mod h1:YZHFjwGCDar1hP9OyBLE46oR7a0Y/Z/0FperDo8+9D0= -github.com/kubescape/k8s-interface v0.0.202 h1:yu9x+07crFQAgrBatFFU2WuuxMJfHUMHVuCzuHE9Q4M= -github.com/kubescape/k8s-interface v0.0.202/go.mod h1:d4NVhL81bVXe8yEXlkT4ZHrt3iEppEIN39b8N1oXm5s= +github.com/kubescape/k8s-interface v0.0.206 h1:qaYu4mlLmSBePanSGq+DBCssh4O785TAT0lQGNGWyGw= +github.com/kubescape/k8s-interface v0.0.206/go.mod h1:WNYUG93aZ5kDmuaRKFLtVhp18Yc6EfaHdD1gLYtVTN4= github.com/kubescape/kubescape-network-scanner v0.0.15 h1:LsaVCQzj0PbA30BeFdzxchW2bkg6nn5quwllWmm/2/s= github.com/kubescape/kubescape-network-scanner v0.0.15/go.mod h1:fqTzRCWsuniGEEZHtOEdITxnqx+i5ICdOVuenSQJd3U= github.com/kubescape/kubescape/v3 v3.0.4 h1:gZ5d8QMxLYZQ6Yz9wRvGcDQlBUIV+v/Y/41g56/YDy8= github.com/kubescape/kubescape/v3 v3.0.4/go.mod h1:upPCVTCRT3+LuZ1bawGtjreRmr/Xa+LT0fHtPzlylRU= -github.com/kubescape/node-agent v0.3.38 h1:eILySwFZ/jCzZH5sYvSu0oPmabJpth2yN3EJUhiGwow= -github.com/kubescape/node-agent v0.3.38/go.mod h1:t2ufPpsMwuBKLWbTeLdHeJcjRUlr2WrBLHWQP5TafpI= +github.com/kubescape/node-agent v0.3.98-0.20260423123245-49b120190105 h1:lQsZxsKVCuBSKS9D6W0s08Bep04SkF3/2IHS8UXIsPE= +github.com/kubescape/node-agent v0.3.98-0.20260423123245-49b120190105/go.mod h1:Pa1WG8QnmrnnIvovS8bHdX6JS6vyNOQHiwEj0nULz2s= github.com/kubescape/opa-utils v0.0.285 h1:iOCwwyqsIz++jqfZHD1WsvHpyNGcUzaLapzrG24aV64= github.com/kubescape/opa-utils v0.0.285/go.mod h1:fgL/Bykj2+HYQc81Mj2HkWrs5tBwIlb+/yEkKfA54fw= github.com/kubescape/rbac-utils v0.0.21-0.20230806101615-07e36f555520 h1:SqlwF8G+oFazeYmZQKoPczLEflBQpwpHCU8DoLLyfj8= github.com/kubescape/rbac-utils v0.0.21-0.20230806101615-07e36f555520/go.mod h1:wuxMUSDzGUyWd25IJfBzEJ/Udmw2Vy7npj+MV3u3GrU= github.com/kubescape/regolibrary v1.0.317-0.20240320124840-1d84ac7186ea h1:hLUe+1bdhiBD7xM/jliQozVd1NLYn1afQLxl5trQdPk= github.com/kubescape/regolibrary v1.0.317-0.20240320124840-1d84ac7186ea/go.mod h1:RK9dHjllKFnISDmVExQlI1B1z93TlQsAu/Kq9c0mt2U= -github.com/kubescape/storage v0.0.239 h1:hfuq1+CuEAKE7zCg9bB8gfU9vZoGMrJBgNh5tAD1rak= -github.com/kubescape/storage v0.0.239/go.mod h1:f6u/Lt3SjUTBrmzOStb33IkKTtaqKM4pyfV5d1lUMiY= +github.com/kubescape/storage v0.0.268 h1:b04rWG7jpJLlUVT43M0AQ3389BxsiW5Pv7rTVFOuBII= +github.com/kubescape/storage v0.0.268/go.mod h1:hUpp5E/GHCNdMxhUDi0h4jPm6HEvyvEOlr4nd4G+Q5A= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -861,8 +861,8 @@ github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 h1:ah1dvbqPMN5+oc github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/picatz/xcel v0.0.0-20250816143731-885b5f678a12 h1:RS7RxrC+OtnYpgI0li0NwvpE0cqYewsZGXUb6wAe0oQ= -github.com/picatz/xcel v0.0.0-20250816143731-885b5f678a12/go.mod h1:jxNaYyVlWe+WPV3G45KzlMLvplS3PQdHLUsFePIcaEg= +github.com/picatz/xcel v0.0.0-20260226001349-6958ffac5706 h1:xfPEUCHSHcjpu4WxgtC1lwBkP4Xa7R/pl8sXVF103Yg= +github.com/picatz/xcel v0.0.0-20260226001349-6958ffac5706/go.mod h1:bFTXcuU+280rICoGMpVTk/06XNfgvfeplhjWWoLKPys= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -1094,8 +1094,8 @@ go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdI go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.etcd.io/etcd/client/v3 v3.6.5 h1:yRwZNFBx/35VKHTcLDeO7XVLbCBFbPi+XV4OC3QJf2U= go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo= -go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= -go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -1540,8 +1540,8 @@ google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdr google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= -google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE= -google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0= +google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= +google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/watcher/containerprofilewatcher.go b/watcher/containerprofilewatcher.go index edac1c1..b5b65ae 100644 --- a/watcher/containerprofilewatcher.go +++ b/watcher/containerprofilewatcher.go @@ -161,13 +161,13 @@ func (wh *WatchHandler) getContainerProfileWatcher() (watch.Interface, error) { } func getPod(client kubernetes.Interface, obj *spdxv1beta1.ContainerProfile) (*corev1.Pod, error) { - if kind, ok := obj.Labels[helpersv1.KindMetadataKey]; !ok || kind != "Pod" { + if kind, ok := obj.Labels[helpersv1.RelatedKindMetadataKey]; !ok || kind != "Pod" { return nil, nil } - podName, ok := obj.Labels[helpersv1.NameMetadataKey] + podName, ok := obj.Labels[helpersv1.RelatedNameMetadataKey] if !ok || podName == "" { - return nil, fmt.Errorf("label %s is missing", helpersv1.NameMetadataKey) + return nil, fmt.Errorf("label %s is missing", helpersv1.RelatedNameMetadataKey) } pod, err := client.CoreV1().Pods(obj.Namespace).Get(context.TODO(), podName, metav1.GetOptions{}) @@ -179,10 +179,10 @@ func (wh *WatchHandler) hasMatchingPod(labels map[string]string) bool { gvr := schema.GroupVersionResource{ Group: labels[helpersv1.ApiGroupMetadataKey], Version: labels[helpersv1.ApiVersionMetadataKey], - Resource: strings.ToLower(labels[helpersv1.KindMetadataKey]) + "s", + Resource: strings.ToLower(labels[helpersv1.RelatedKindMetadataKey]) + "s", } - name := labels[helpersv1.NameMetadataKey] - namespace := labels[helpersv1.NamespaceMetadataKey] + name := labels[helpersv1.RelatedNameMetadataKey] + namespace := labels[helpersv1.RelatedNamespaceMetadataKey] // get the unstructured workload object workloadObj, err := wh.k8sAPI.DynamicClient.Resource(gvr).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { diff --git a/watcher/containerprofilewatcher_test.go b/watcher/containerprofilewatcher_test.go index 53cf78d..b3642da 100644 --- a/watcher/containerprofilewatcher_test.go +++ b/watcher/containerprofilewatcher_test.go @@ -92,8 +92,8 @@ func TestHandleContainerProfileEvents(t *testing.T) { helpersv1.StatusMetadataKey: helpersv1.Learning, }, Labels: map[string]string{ - helpersv1.KindMetadataKey: "Pod", - helpersv1.NameMetadataKey: "foo-1747274700", + helpersv1.RelatedKindMetadataKey: "Pod", + helpersv1.RelatedNameMetadataKey: "foo-1747274700", }, }, Spec: spdxv1beta1.ContainerProfileSpec{ @@ -115,8 +115,8 @@ func TestHandleContainerProfileEvents(t *testing.T) { helpersv1.StatusMetadataKey: helpersv1.Learning, }, Labels: map[string]string{ - helpersv1.KindMetadataKey: "Pod", - helpersv1.NameMetadataKey: "foo2-2747274700", + helpersv1.RelatedKindMetadataKey: "Pod", + helpersv1.RelatedNameMetadataKey: "foo2-2747274700", }, }, Spec: spdxv1beta1.ContainerProfileSpec{ @@ -366,9 +366,9 @@ func TestWatchHandler_hasMatchingPod(t *testing.T) { labels: map[string]string{ helpersv1.ApiGroupMetadataKey: "apps", helpersv1.ApiVersionMetadataKey: "v1", - helpersv1.KindMetadataKey: "Deployment", - helpersv1.NameMetadataKey: "nginx-deployment", - helpersv1.NamespaceMetadataKey: "web", + helpersv1.RelatedKindMetadataKey: "Deployment", + helpersv1.RelatedNameMetadataKey: "nginx-deployment", + helpersv1.RelatedNamespaceMetadataKey: "web", }, want: true, }, @@ -377,9 +377,9 @@ func TestWatchHandler_hasMatchingPod(t *testing.T) { labels: map[string]string{ helpersv1.ApiGroupMetadataKey: "apps", helpersv1.ApiVersionMetadataKey: "v1", - helpersv1.KindMetadataKey: "Deployment", - helpersv1.NameMetadataKey: "nginx-deployment", - helpersv1.NamespaceMetadataKey: "other", + helpersv1.RelatedKindMetadataKey: "Deployment", + helpersv1.RelatedNameMetadataKey: "nginx-deployment", + helpersv1.RelatedNamespaceMetadataKey: "other", }, want: false, }, @@ -388,9 +388,9 @@ func TestWatchHandler_hasMatchingPod(t *testing.T) { labels: map[string]string{ helpersv1.ApiGroupMetadataKey: "apps", helpersv1.ApiVersionMetadataKey: "v1", - helpersv1.KindMetadataKey: "Deployment", - helpersv1.NameMetadataKey: "empty-deployment", - helpersv1.NamespaceMetadataKey: "web", + helpersv1.RelatedKindMetadataKey: "Deployment", + helpersv1.RelatedNameMetadataKey: "empty-deployment", + helpersv1.RelatedNamespaceMetadataKey: "web", }, want: false, }, From 0d1620e598a3b1fd2d5212428da051ef094d3d02 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 15 Jun 2026 07:22:27 +0200 Subject: [PATCH 3/3] feat(openprotection): publish active-rule open-protection union as ConfigMap Add a watcher that keeps the storage apiserver's collapse-protection in sync with the cluster's active runtime rules (design B: active-CRD union). The operator watches RuntimeRuleAlertBinding, resolves the rules each binding activates (by id/name/tags) against the versioned rule library (armosec/rulelibrary), computes the union of those rules' profileDataRequired.opens (armotypes.OpenMatchers), and publishes it as a single ConfigMap (key "openProtection"). The storage apiserver polls that ConfigMap and pins the matched sensitive prefixes to literal during profile collapse so anomaly rules such as R0010 ("unexpected /etc/shadow access") keep working. This is the producer side of "operator writes one object, storage refreshes periodically"; the reader lives in kubescape/storage. Design notes: - Independent of the admission controller: runs its own dynamic watcher so protection works regardless of admission config. Gated by the new openProtectionConfigMapName config (empty = disabled). - Reconciles are debounced (coalesce the initial LIST burst) and idempotent (canonical, sorted/de-duplicated JSON; skip the write when unchanged, both in the watcher and the publisher). - No bindings -> empty union (no over-pinning). Deleting the last binding that pinned a prefix drops it on the next reconcile. Resolving selectors in the library (not node-agent's rule engine) keeps the set scoped to actually-active rules. Unit tests cover selector extraction, the union end-to-end against the embedded library (R0010 -> /etc/shadow), idempotence, empty/delete transitions, and the ConfigMap publisher (create/update/skip-unchanged) via a fake clientset. NOTE: depends on rulelibrary pkg/rules (PR #93), pinned here to its branch commit. Landing requires #93 merged and rulelibrary tagged with a release that contains pkg/rules; until then `go mod tidy` will try to resolve rulelibrary to the older release tag that lacks the package. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Matthias Bertschy --- config/config.go | 10 ++ go.mod | 1 + go.sum | 2 + main.go | 16 ++ openprotection/publisher.go | 74 +++++++++ openprotection/publisher_test.go | 64 ++++++++ openprotection/watcher.go | 253 +++++++++++++++++++++++++++++++ openprotection/watcher_test.go | 177 +++++++++++++++++++++ 8 files changed, 597 insertions(+) create mode 100644 openprotection/publisher.go create mode 100644 openprotection/publisher_test.go create mode 100644 openprotection/watcher.go create mode 100644 openprotection/watcher_test.go diff --git a/config/config.go b/config/config.go index 8232e2a..3c5694d 100644 --- a/config/config.go +++ b/config/config.go @@ -132,6 +132,11 @@ type Config struct { RulesUpdateConfig rulesupdate.RulesUpdaterConfig `mapstructure:"rulesUpdateConfig"` SkipProfilesWithoutInstances bool `mapstructure:"skipProfilesWithoutInstances"` NodeAgentAutoscaler NodeAgentAutoscalerConfig `mapstructure:"nodeAgentAutoscaler"` + // OpenProtectionConfigMapName, when non-empty, enables the open-protection + // watcher: the operator resolves active RuntimeRuleAlertBinding selectors to + // the union of their profileDataRequired.opens and publishes it into this + // ConfigMap (in the operator namespace) for the storage apiserver to read. + OpenProtectionConfigMapName string `mapstructure:"openProtectionConfigMapName"` } // IConfig is an interface for all config types used in the operator @@ -158,6 +163,7 @@ type IConfig interface { SkipProfilesWithoutInstances() bool RulesUpdateEnabled() bool NodeAgentAutoscalerConfig() NodeAgentAutoscalerConfig + OpenProtectionConfigMapName() string } // OperatorConfig implements IConfig @@ -213,6 +219,10 @@ func (c *OperatorConfig) Namespace() string { return c.serviceConfig.Namespace } +func (c *OperatorConfig) OpenProtectionConfigMapName() string { + return c.serviceConfig.OpenProtectionConfigMapName +} + func (c *OperatorConfig) CleanUpRoutineInterval() time.Duration { return c.serviceConfig.CleanUpRoutineInterval } diff --git a/go.mod b/go.mod index f1ebb1a..bdf7528 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.8 require ( github.com/armosec/armoapi-go v0.0.719 github.com/armosec/registryx v0.0.35 + github.com/armosec/rulelibrary v0.0.71-0.20260612121633-c246d476b172 github.com/armosec/utils-go v0.0.58 github.com/armosec/utils-k8s-go v0.0.35 github.com/aws/aws-sdk-go-v2 v1.41.5 diff --git a/go.sum b/go.sum index 4a42f63..c2f2a43 100644 --- a/go.sum +++ b/go.sum @@ -151,6 +151,8 @@ github.com/armosec/gojay v1.2.17 h1:VSkLBQzD1c2V+FMtlGFKqWXNsdNvIKygTKJI9ysY8eM= github.com/armosec/gojay v1.2.17/go.mod h1:vuvX3DlY0nbVrJ0qCklSS733AWMoQboq3cFyuQW9ybc= github.com/armosec/registryx v0.0.35 h1:fvP5/IZL0+jJUnNXvAj6ohJ6UVHaASVpR/cPLX0I4f0= github.com/armosec/registryx v0.0.35/go.mod h1:dm7/1yJllY2MDS8oxGyR+64NAepwDXV4/2Q5mnD8yHs= +github.com/armosec/rulelibrary v0.0.71-0.20260612121633-c246d476b172 h1:eJUCl55H8jA5LrMct1twVtct9N33Hag8xOfnlQd3Iz8= +github.com/armosec/rulelibrary v0.0.71-0.20260612121633-c246d476b172/go.mod h1:AWpfYz8VAAXtp1koZ0AdxlHejO5Nw7clePuLGHg+Jcs= github.com/armosec/utils-go v0.0.58 h1:g9RnRkxZAmzTfPe2ruMo2OXSYLwVSegQSkSavOfmaIE= github.com/armosec/utils-go v0.0.58/go.mod h1:CdqKHKruVJMCxGcZXYW9J+5P9FZou8dMzVpcB0Xt8pk= github.com/armosec/utils-k8s-go v0.0.35 h1:CliNObhAca5UYl84m5OQecOTm9ZfMFI8648pYhQJiu4= diff --git a/main.go b/main.go index f058f5e..40a8ac0 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,7 @@ import ( "github.com/kubescape/operator/mainhandler" "github.com/kubescape/operator/nodeagentautoscaler" "github.com/kubescape/operator/objectcache" + "github.com/kubescape/operator/openprotection" "github.com/kubescape/operator/restapihandler" "github.com/kubescape/operator/servicehandler" "github.com/kubescape/operator/utils" @@ -220,6 +221,21 @@ func main() { defer dWatcher.Stop(ctx) } + // Open-protection watcher: independent of the admission controller. It watches + // RuntimeRuleAlertBinding, resolves the active rules' profileDataRequired.opens + // against the rule library, and publishes their union into a ConfigMap that the + // storage apiserver reads to pin sensitive prefixes during profile collapse. + if cmName := operatorConfig.OpenProtectionConfigMapName(); cmName != "" { + opWatcher := dynamicwatcher.NewWatchHandler(k8sApi, ksStorageClient.SpdxV1beta1(), operatorConfig.SkipNamespace) + publisher := openprotection.NewConfigMapPublisher(k8sApi.GetKubernetesClient(), operatorConfig.Namespace(), cmName) + protectionWatcher := openprotection.NewWatcher(publisher, openprotection.DefaultDebounce) + opWatcher.AddAdaptor(protectionWatcher) + + go protectionWatcher.Run(ctx) + opWatcher.Start(ctx) + defer opWatcher.Stop(ctx) + } + if logger.L().GetLevel() == helpers.DebugLevel.String() { go func() { logger.L().Info("starting pprof server", helpers.String("port", "6060")) diff --git a/openprotection/publisher.go b/openprotection/publisher.go new file mode 100644 index 0000000..18c6869 --- /dev/null +++ b/openprotection/publisher.go @@ -0,0 +1,74 @@ +package openprotection + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// ConfigMapKey is the key under which the open-protection union JSON is stored. +// It MUST match the storage apiserver's reader constant +// (file.OpenProtectionConfigMapKey = "openProtection"); the two repos agree on +// this string by convention since storage cannot import the operator. +const ConfigMapKey = "openProtection" + +// managedByLabel marks the ConfigMap as operator-owned so it is easy to identify +// and so we never clobber an unrelated ConfigMap of the same name. +const ( + managedByLabelKey = "app.kubernetes.io/managed-by" + managedByLabelValue = "operator-open-protection" +) + +// Publisher writes the open-protection union into a ConfigMap that the storage +// apiserver polls. It is the sink side of the watcher: the watcher computes the +// union from active rule bindings, the publisher reconciles it into the cluster. +type Publisher interface { + Publish(ctx context.Context, payload string) error +} + +// ConfigMapPublisher reconciles a single ConfigMap (namespace/name) so its +// ConfigMapKey entry equals payload, creating the ConfigMap if absent and +// skipping the write when already up to date. +type ConfigMapPublisher struct { + client kubernetes.Interface + namespace string + name string +} + +var _ Publisher = (*ConfigMapPublisher)(nil) + +func NewConfigMapPublisher(client kubernetes.Interface, namespace, name string) *ConfigMapPublisher { + return &ConfigMapPublisher{client: client, namespace: namespace, name: name} +} + +func (p *ConfigMapPublisher) Publish(ctx context.Context, payload string) error { + cms := p.client.CoreV1().ConfigMaps(p.namespace) + cm, err := cms.Get(ctx, p.name, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + _, err = cms.Create(ctx, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: p.name, + Namespace: p.namespace, + Labels: map[string]string{managedByLabelKey: managedByLabelValue}, + }, + Data: map[string]string{ConfigMapKey: payload}, + }, metav1.CreateOptions{}) + return err + } + if err != nil { + return err + } + if cm.Data[ConfigMapKey] == payload { + // already up to date — avoid a no-op write (and a needless resourceVersion bump) + return nil + } + if cm.Data == nil { + cm.Data = map[string]string{} + } + cm.Data[ConfigMapKey] = payload + _, err = cms.Update(ctx, cm, metav1.UpdateOptions{}) + return err +} diff --git a/openprotection/publisher_test.go b/openprotection/publisher_test.go new file mode 100644 index 0000000..e608dca --- /dev/null +++ b/openprotection/publisher_test.go @@ -0,0 +1,64 @@ +package openprotection + +import ( + "context" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestConfigMapPublisherCreatesWhenAbsent(t *testing.T) { + client := fake.NewSimpleClientset() + p := NewConfigMapPublisher(client, "kubescape", "storage-open-protection") + + if err := p.Publish(context.Background(), `{"prefix":["/etc/shadow"]}`); err != nil { + t.Fatalf("publish: %v", err) + } + cm, err := client.CoreV1().ConfigMaps("kubescape").Get(context.Background(), "storage-open-protection", metav1.GetOptions{}) + if err != nil { + t.Fatalf("get: %v", err) + } + if cm.Data[ConfigMapKey] != `{"prefix":["/etc/shadow"]}` { + t.Errorf("unexpected data: %q", cm.Data[ConfigMapKey]) + } + if cm.Labels[managedByLabelKey] != managedByLabelValue { + t.Errorf("expected managed-by label, got %v", cm.Labels) + } +} + +func TestConfigMapPublisherUpdatesExisting(t *testing.T) { + client := fake.NewSimpleClientset(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Namespace: "kubescape", Name: "storage-open-protection"}, + Data: map[string]string{ConfigMapKey: `{"prefix":["/old"]}`, "keepme": "yes"}, + }) + p := NewConfigMapPublisher(client, "kubescape", "storage-open-protection") + + if err := p.Publish(context.Background(), `{"prefix":["/new"]}`); err != nil { + t.Fatalf("publish: %v", err) + } + cm, _ := client.CoreV1().ConfigMaps("kubescape").Get(context.Background(), "storage-open-protection", metav1.GetOptions{}) + if cm.Data[ConfigMapKey] != `{"prefix":["/new"]}` { + t.Errorf("expected updated payload, got %q", cm.Data[ConfigMapKey]) + } + if cm.Data["keepme"] != "yes" { + t.Errorf("publisher clobbered unrelated key") + } +} + +func TestConfigMapPublisherSkipsUnchanged(t *testing.T) { + client := fake.NewSimpleClientset(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Namespace: "kubescape", Name: "storage-open-protection", ResourceVersion: "7"}, + Data: map[string]string{ConfigMapKey: `{"prefix":["/same"]}`}, + }) + p := NewConfigMapPublisher(client, "kubescape", "storage-open-protection") + + if err := p.Publish(context.Background(), `{"prefix":["/same"]}`); err != nil { + t.Fatalf("publish: %v", err) + } + cm, _ := client.CoreV1().ConfigMaps("kubescape").Get(context.Background(), "storage-open-protection", metav1.GetOptions{}) + if cm.ResourceVersion != "7" { + t.Errorf("expected no write for unchanged payload (resourceVersion bumped to %s)", cm.ResourceVersion) + } +} diff --git a/openprotection/watcher.go b/openprotection/watcher.go new file mode 100644 index 0000000..06ca18f --- /dev/null +++ b/openprotection/watcher.go @@ -0,0 +1,253 @@ +// Package openprotection keeps the storage apiserver's collapse-protection in +// sync with the cluster's active runtime rules. +// +// The operator watches RuntimeRuleAlertBinding objects, resolves the rules they +// activate against the versioned rule library (armosec/rulelibrary), computes the +// union of those rules' profileDataRequired.opens (armotypes.OpenMatchers), and +// publishes it as a single ConfigMap. The storage apiserver polls that ConfigMap +// and pins the matched sensitive prefixes to literal during profile collapse, so +// anomaly rules such as R0010 ("unexpected /etc/shadow access") keep working. +// +// This is the producer side of the "operator writes one object, storage refreshes +// periodically" design; the reader lives in kubescape/storage. +package openprotection + +import ( + "context" + "encoding/json" + "sort" + "sync" + "time" + + "github.com/armosec/armoapi-go/armotypes" + rulelib "github.com/armosec/rulelibrary/pkg/rules" + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" + typesv1 "github.com/kubescape/node-agent/pkg/rulebindingmanager/types/v1" + nodeagentutils "github.com/kubescape/node-agent/pkg/utils" + "github.com/kubescape/node-agent/pkg/watcher" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// DefaultDebounce coalesces bursts of binding events (e.g. the initial LIST that +// fires one Add per existing binding) into a single reconcile. +const DefaultDebounce = 2 * time.Second + +var _ watcher.Adaptor = (*Watcher)(nil) + +// Watcher is a watcher.Adaptor over RuntimeRuleAlertBinding. It maintains the set +// of rule selectors contributed by every binding and, on change, reconciles the +// published open-protection union (debounced). +type Watcher struct { + publisher Publisher + debounce time.Duration + + mu sync.Mutex + bindings map[string]rulelib.RuleSelector // binding uniqueName -> its selectors + + dirty chan struct{} + + // lastApplied is the last payload we published; reconcile skips the publish + // when the recomputed payload is unchanged. + lastApplied string + haveApplied bool +} + +func NewWatcher(publisher Publisher, debounce time.Duration) *Watcher { + if debounce <= 0 { + debounce = DefaultDebounce + } + return &Watcher{ + publisher: publisher, + debounce: debounce, + bindings: map[string]rulelib.RuleSelector{}, + dirty: make(chan struct{}, 1), + } +} + +// ----------------- watcher.WatchResources ----------------- + +func (w *Watcher) WatchResources() []watcher.WatchResource { + return []watcher.WatchResource{ + watcher.NewWatchResource(typesv1.RuleBindingAlertGvr, metav1.ListOptions{}), + } +} + +// ----------------- watcher.Watcher (event handlers) ----------------- + +func (w *Watcher) AddHandler(_ context.Context, obj runtime.Object) { w.upsert(obj) } + +func (w *Watcher) ModifyHandler(_ context.Context, obj runtime.Object) { w.upsert(obj) } + +func (w *Watcher) DeleteHandler(_ context.Context, obj runtime.Object) { + un, ok := obj.(*unstructured.Unstructured) + if !ok { + return + } + key := nodeagentutils.CreateK8sPodID(un.GetNamespace(), un.GetName()) + w.mu.Lock() + _, existed := w.bindings[key] + delete(w.bindings, key) + w.mu.Unlock() + if existed { + w.markDirty() + } +} + +func (w *Watcher) upsert(obj runtime.Object) { + un, ok := obj.(*unstructured.Unstructured) + if !ok { + return + } + rb := &typesv1.RuntimeAlertRuleBinding{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, rb); err != nil { + logger.L().Error("openprotection: failed to convert rule binding", helpers.Error(err)) + return + } + key := nodeagentutils.CreateK8sPodID(rb.GetNamespace(), rb.GetName()) + sel := bindingSelector(rb) + w.mu.Lock() + w.bindings[key] = sel + w.mu.Unlock() + w.markDirty() +} + +// bindingSelector extracts the (ids, names, tags) a binding activates. A binding +// rule references a library rule by id, name, or tags; we collect all of them so +// the union below resolves exactly the rules this binding turns on. +func bindingSelector(rb *typesv1.RuntimeAlertRuleBinding) rulelib.RuleSelector { + var sel rulelib.RuleSelector + for _, r := range rb.Spec.Rules { + if r.RuleID != "" { + sel.IDs = append(sel.IDs, r.RuleID) + } + if r.RuleName != "" { + sel.Names = append(sel.Names, r.RuleName) + } + sel.Tags = append(sel.Tags, r.RuleTags...) + } + return sel +} + +// ----------------- reconcile loop ----------------- + +func (w *Watcher) markDirty() { + select { + case w.dirty <- struct{}{}: + default: // already pending — coalesce + } +} + +// Run drives the debounced reconcile loop until ctx is cancelled. It performs an +// initial reconcile after the first debounce window so an empty cluster (no +// bindings) deterministically publishes an empty union, then reconciles whenever +// bindings change. +func (w *Watcher) Run(ctx context.Context) { + timer := time.NewTimer(w.debounce) + defer timer.Stop() + pending := true // schedule an initial reconcile even with no events + for { + select { + case <-ctx.Done(): + return + case <-w.dirty: + if !pending { + pending = true + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + timer.Reset(w.debounce) + } + case <-timer.C: + if pending { + pending = false + w.reconcile(ctx) + } + timer.Reset(w.debounce) + } + } +} + +// reconcile recomputes the union across all bindings and publishes it if changed. +func (w *Watcher) reconcile(ctx context.Context) { + w.mu.Lock() + merged := mergeSelectors(w.bindings) + w.mu.Unlock() + + m, err := rulelib.OpenProtectionForSelectors(merged) + if err != nil { + logger.L().Ctx(ctx).Error("openprotection: failed to resolve open protection", helpers.Error(err)) + return + } + payload, err := marshalCanonical(m) + if err != nil { + logger.L().Ctx(ctx).Error("openprotection: failed to marshal open matchers", helpers.Error(err)) + return + } + + w.mu.Lock() + unchanged := w.haveApplied && w.lastApplied == payload + w.mu.Unlock() + if unchanged { + return + } + + if err := w.publisher.Publish(ctx, payload); err != nil { + logger.L().Ctx(ctx).Error("openprotection: failed to publish configmap", helpers.Error(err)) + return + } + w.mu.Lock() + w.lastApplied = payload + w.haveApplied = true + w.mu.Unlock() + logger.L().Info("openprotection: published open-protection union", helpers.Int("bytes", len(payload))) +} + +// mergeSelectors flattens every binding's selectors into one selector; the +// library unions across all rules any binding activates. +func mergeSelectors(bindings map[string]rulelib.RuleSelector) rulelib.RuleSelector { + var out rulelib.RuleSelector + for _, s := range bindings { + out.IDs = append(out.IDs, s.IDs...) + out.Names = append(out.Names, s.Names...) + out.Tags = append(out.Tags, s.Tags...) + } + return out +} + +// marshalCanonical produces a stable JSON encoding (sorted, de-duplicated slices) +// so an unchanged active-rule set always yields byte-identical output — that is +// what lets reconcile and the publisher skip no-op writes. +func marshalCanonical(m armotypes.OpenMatchers) (string, error) { + m.Exact = sortedUnique(m.Exact) + m.Prefix = sortedUnique(m.Prefix) + m.Suffix = sortedUnique(m.Suffix) + m.Contains = sortedUnique(m.Contains) + b, err := json.Marshal(m) + if err != nil { + return "", err + } + return string(b), nil +} + +func sortedUnique(in []string) []string { + if len(in) == 0 { + return in + } + seen := make(map[string]struct{}, len(in)) + out := make([]string, 0, len(in)) + for _, s := range in { + if _, ok := seen[s]; ok { + continue + } + seen[s] = struct{}{} + out = append(out, s) + } + sort.Strings(out) + return out +} diff --git a/openprotection/watcher_test.go b/openprotection/watcher_test.go new file mode 100644 index 0000000..75ab477 --- /dev/null +++ b/openprotection/watcher_test.go @@ -0,0 +1,177 @@ +package openprotection + +import ( + "context" + "encoding/json" + "testing" + + "github.com/armosec/armoapi-go/armotypes" + rulelib "github.com/armosec/rulelibrary/pkg/rules" + typesv1 "github.com/kubescape/node-agent/pkg/rulebindingmanager/types/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +type capturePublisher struct { + payloads []string +} + +func (c *capturePublisher) Publish(_ context.Context, payload string) error { + c.payloads = append(c.payloads, payload) + return nil +} + +func (c *capturePublisher) last() string { + if len(c.payloads) == 0 { + return "" + } + return c.payloads[len(c.payloads)-1] +} + +func bindingUnstructured(t *testing.T, namespace, name string, rules []typesv1.RuntimeAlertRuleBindingRule) *unstructured.Unstructured { + t.Helper() + rb := &typesv1.RuntimeAlertRuleBinding{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name}, + Spec: typesv1.RuntimeAlertRuleBindingSpec{Rules: rules}, + } + m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(rb) + if err != nil { + t.Fatalf("to unstructured: %v", err) + } + return &unstructured.Unstructured{Object: m} +} + +func TestBindingSelector(t *testing.T) { + rb := &typesv1.RuntimeAlertRuleBinding{ + Spec: typesv1.RuntimeAlertRuleBindingSpec{ + Rules: []typesv1.RuntimeAlertRuleBindingRule{ + {RuleID: "R0010"}, + {RuleName: "Unexpected process"}, + {RuleTags: []string{"sensitive", "files"}}, + }, + }, + } + sel := bindingSelector(rb) + if len(sel.IDs) != 1 || sel.IDs[0] != "R0010" { + t.Errorf("IDs = %v", sel.IDs) + } + if len(sel.Names) != 1 || sel.Names[0] != "Unexpected process" { + t.Errorf("Names = %v", sel.Names) + } + if len(sel.Tags) != 2 { + t.Errorf("Tags = %v", sel.Tags) + } +} + +func TestMarshalCanonicalIsStableAndDeduped(t *testing.T) { + a, err := marshalCanonical(armotypes.OpenMatchers{Prefix: []string{"/b", "/a", "/a"}}) + if err != nil { + t.Fatal(err) + } + b, err := marshalCanonical(armotypes.OpenMatchers{Prefix: []string{"/a", "/b"}}) + if err != nil { + t.Fatal(err) + } + if a != b { + t.Fatalf("canonical output not stable: %q vs %q", a, b) + } + var got armotypes.OpenMatchers + if err := json.Unmarshal([]byte(a), &got); err != nil { + t.Fatal(err) + } + if len(got.Prefix) != 2 || got.Prefix[0] != "/a" || got.Prefix[1] != "/b" { + t.Errorf("expected sorted-unique [/a /b], got %v", got.Prefix) + } +} + +// TestReconcilePublishesUnionForBoundRule drives the watcher end-to-end against +// the real embedded rule library: binding R0010 must publish a union that keeps +// /etc/shadow detectable. +func TestReconcilePublishesUnionForBoundRule(t *testing.T) { + pub := &capturePublisher{} + w := NewWatcher(pub, 0) + + w.AddHandler(context.Background(), bindingUnstructured(t, "kubescape", "binding-1", + []typesv1.RuntimeAlertRuleBindingRule{{RuleID: "R0010"}})) + w.reconcile(context.Background()) + + if len(pub.payloads) != 1 { + t.Fatalf("expected one publish, got %d", len(pub.payloads)) + } + var m armotypes.OpenMatchers + if err := json.Unmarshal([]byte(pub.last()), &m); err != nil { + t.Fatal(err) + } + if !containsStr(m.Prefix, "/etc/shadow") { + t.Errorf("expected /etc/shadow in published prefix union, got %+v", m) + } + + // Reconcile again with no change: must NOT publish again (idempotent). + w.reconcile(context.Background()) + if len(pub.payloads) != 1 { + t.Errorf("expected no second publish for unchanged set, got %d", len(pub.payloads)) + } +} + +// TestReconcileEmptyWhenNoBindings: with no bindings, the published union is empty +// (no over-pinning). +func TestReconcileEmptyWhenNoBindings(t *testing.T) { + pub := &capturePublisher{} + w := NewWatcher(pub, 0) + w.reconcile(context.Background()) + if len(pub.payloads) != 1 { + t.Fatalf("expected one publish, got %d", len(pub.payloads)) + } + var m armotypes.OpenMatchers + if err := json.Unmarshal([]byte(pub.last()), &m); err != nil { + t.Fatal(err) + } + if !m.Empty() { + t.Errorf("expected empty union with no bindings, got %+v", m) + } +} + +// TestDeleteRemovesContribution: deleting the only binding that pinned a prefix +// drops it from the next union. +func TestDeleteRemovesContribution(t *testing.T) { + pub := &capturePublisher{} + w := NewWatcher(pub, 0) + bind := bindingUnstructured(t, "kubescape", "binding-1", + []typesv1.RuntimeAlertRuleBindingRule{{RuleID: "R0010"}}) + + w.AddHandler(context.Background(), bind) + w.reconcile(context.Background()) + var withRule armotypes.OpenMatchers + _ = json.Unmarshal([]byte(pub.last()), &withRule) + if !containsStr(withRule.Prefix, "/etc/shadow") { + t.Fatalf("precondition: expected /etc/shadow while bound, got %+v", withRule) + } + + w.DeleteHandler(context.Background(), bind) + w.reconcile(context.Background()) + var afterDelete armotypes.OpenMatchers + _ = json.Unmarshal([]byte(pub.last()), &afterDelete) + if containsStr(afterDelete.Prefix, "/etc/shadow") { + t.Errorf("expected /etc/shadow removed after unbinding, got %+v", afterDelete) + } +} + +func TestMergeSelectors(t *testing.T) { + merged := mergeSelectors(map[string]rulelib.RuleSelector{ + "a": {IDs: []string{"R0001"}, Tags: []string{"x"}}, + "b": {IDs: []string{"R0010"}, Names: []string{"n"}}, + }) + if len(merged.IDs) != 2 || len(merged.Names) != 1 || len(merged.Tags) != 1 { + t.Errorf("unexpected merge: %+v", merged) + } +} + +func containsStr(ss []string, v string) bool { + for _, s := range ss { + if s == v { + return true + } + } + return false +}