diff --git a/README.md b/README.md index 34dde70..bf2611e 100644 --- a/README.md +++ b/README.md @@ -312,9 +312,37 @@ The Operator reads configuration from `/etc/config/`. When running locally, set | `includeNamespaces` | `""` | Comma-separated string or array of exact namespaces to include | | `includeNamespacesRegex` | `""` | Comma-separated string or array of RE2 regex patterns for namespaces to include | | `excludeNamespaces` | `"kube-system,kubescape"` | Comma-separated string or array of exact namespaces to exclude | +| `excludeControls` | `[]` | Control IDs or legacy CIS section numbers omitted from operator posture scans | | `excludeNamespacesRegex` | `""` | Comma-separated string or array of RE2 regex patterns for namespaces to exclude | | `namespaceFilterConfigMapName` | `""` | Optional ConfigMap name in the operator namespace for live namespace filtering | +### Exclude controls from posture scans + +Set `excludeControls` in the operator's `config.json` to a list of control IDs +(or legacy CIS section numbers): + +```json +{"excludeControls": ["C-0069", "C-0070"]} +``` + +The operator forwards these settings for explicit, scheduled, and continuous +scans, including resource deletion scans. Individual `scanV1` requests can add +`excludeControls`; the installation and request lists are combined. An empty +request list does not clear installation settings. Whitespace is trimmed and +duplicates are removed without regard to case, including scheduled replays. +Stored jobs retain only request-specific exclusions; installation settings are +applied when they run. Restart the operator after changing its config file. +Blank identifiers and values other than a list of strings are rejected. + +A scanner supporting this field applies the existing `--exclude-controls` +filter before evaluating controls and computing compliance scores. Excluding a +control narrows the assessment scope; it does not assert that the check passed. +Omitting the installation setting leaves existing behavior unchanged. + +Compatibility: external implementations of `config.IConfig` must also provide +`ExcludeControls() []string`. Returning `nil` or an empty slice keeps scans +without installation-level exclusions. + ### Namespace Filtering (Exact & Regex) The Operator supports filtering namespaces using exact names (`includeNamespaces`, `excludeNamespaces`) and RE2 regular expressions (`includeNamespacesRegex`, `excludeNamespacesRegex`): diff --git a/config/config.go b/config/config.go index 0b46cc8..2feeb38 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,7 @@ package config import ( + "encoding/json" "fmt" "os" "regexp" @@ -131,6 +132,7 @@ func LoadCapabilitiesConfig(path string) (CapabilitiesConfig, error) { } type Config struct { + ExcludeControls []string `mapstructure:"excludeControls"` NamespaceFilterConfigMapName string `mapstructure:"namespaceFilterConfigMapName"` Namespace string `mapstructure:"namespace"` RestAPIPort string `mapstructure:"port"` @@ -183,6 +185,7 @@ type IConfig interface { // DefaultFrameworks returns install-time posture frameworks from clusterData. // Empty means callers should keep their legacy fallback (e.g. "all" or the native trio). DefaultFrameworks() []string + ExcludeControls() []string } // OperatorConfig implements IConfig @@ -303,6 +306,10 @@ func (c *OperatorConfig) ClusterName() string { return c.clusterConfig.ClusterName } +func (c *OperatorConfig) ExcludeControls() []string { + return slices.Clone(c.serviceConfig.ExcludeControls) +} + func (c *OperatorConfig) DefaultFrameworks() []string { out := make([]string, 0, len(c.clusterConfig.DefaultFrameworks)) for _, f := range c.clusterConfig.DefaultFrameworks { @@ -436,6 +443,22 @@ func LoadConfig(path string) (Config, error) { return Config{}, err } + // Viper weakly converts scalars and numbers; decode this list strictly. + if raw := viper.Get("excludeControls"); raw != nil { + data, err := json.Marshal(raw) + if err != nil { + return Config{}, fmt.Errorf("invalid excludeControls: %w", err) + } + if err := json.Unmarshal(data, &c.ExcludeControls); err != nil { + return Config{}, fmt.Errorf("invalid excludeControls: %w", err) + } + } + for _, control := range c.ExcludeControls { + if strings.TrimSpace(control) == "" { + return Config{}, fmt.Errorf("excludeControls contains an empty control identifier") + } + } + if _, err := compileRegexes(c.IncludeNamespacesRegex); err != nil { return Config{}, fmt.Errorf("invalid includeNamespacesRegex: %w", err) } diff --git a/config/control_exclusions_test.go b/config/control_exclusions_test.go new file mode 100644 index 0000000..59944a6 --- /dev/null +++ b/config/control_exclusions_test.go @@ -0,0 +1,50 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + + utilsmetadata "github.com/armosec/utils-k8s-go/armometadata" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +func TestLoadControlExclusions(t *testing.T) { + for _, tt := range []struct { + name, input string + want []string + wantError bool + }{ + {"absent", `{}`, nil, false}, + {"empty", `{"excludeControls":[]}`, []string{}, false}, + {"null", `{"excludeControls":null}`, nil, false}, + {"list", `{"excludeControls":["C-0069","C-0070"]}`, []string{"C-0069", "C-0070"}, false}, + {"scalar", `{"excludeControls":"C-0069"}`, nil, true}, + {"number", `{"excludeControls":[42]}`, nil, true}, + {"blank", `{"excludeControls":[" "]}`, nil, true}, + {"null entry", `{"excludeControls":[null]}`, nil, true}, + } { + t.Run(tt.name, func(t *testing.T) { + viper.Reset() + t.Cleanup(viper.Reset) + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), []byte(tt.input), 0600)) + got, err := LoadConfig(dir) + if tt.wantError { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got.ExcludeControls) + cfg, err := NewOperatorConfig(CapabilitiesConfig{}, utilsmetadata.ClusterConfig{}, nil, got) + require.NoError(t, err) + exclusions := cfg.ExcludeControls() + require.Equal(t, tt.want, exclusions) + if len(exclusions) > 0 { + exclusions[0] = "changed" + require.Equal(t, tt.want, cfg.ExcludeControls()) + } + }) + } +} diff --git a/go.mod b/go.mod index 78edaa6..4c81827 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/kubescape/k8s-interface v0.0.214 github.com/kubescape/kubescape-network-scanner v0.0.15 github.com/kubescape/node-agent v0.3.38 - github.com/kubescape/opa-utils v0.0.285 + github.com/kubescape/opa-utils v0.0.315 github.com/kubescape/storage v0.0.301 github.com/panjf2000/ants/v2 v2.11.3 github.com/spf13/viper v1.21.0 diff --git a/go.sum b/go.sum index c37f972..265eeef 100644 --- a/go.sum +++ b/go.sum @@ -710,6 +710,8 @@ github.com/kubescape/node-agent v0.3.38 h1:eILySwFZ/jCzZH5sYvSu0oPmabJpth2yN3EJU github.com/kubescape/node-agent v0.3.38/go.mod h1:t2ufPpsMwuBKLWbTeLdHeJcjRUlr2WrBLHWQP5TafpI= 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/opa-utils v0.0.315 h1:43b6beRDo74nMztOFUBLWV0Vr2T0Ugap7/goU3JPe0Q= +github.com/kubescape/opa-utils v0.0.315/go.mod h1:dWNcjmiTwGYlNmclXvDvgH7UlLdpDvmEc9T7ACTqLo0= 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= diff --git a/mainhandler/agent_runtime_requests_test.go b/mainhandler/agent_runtime_requests_test.go index 722a63e..127ca09 100644 --- a/mainhandler/agent_runtime_requests_test.go +++ b/mainhandler/agent_runtime_requests_test.go @@ -36,6 +36,7 @@ func TestAgentRuntimeFrameworkRequestSurvivesScheduledTrigger(t *testing.T) { "includeNamespaces": []string{"agents"}, "excludedNamespaces": []string{"kube-system"}, "hostScanner": false, + "excludeControls": []string{"C-0070"}, "keepLocal": true, "useCachedArtifacts": true, "exceptions": []interface{}{map[string]interface{}{"name": "agent-runtime-exception"}}, @@ -47,11 +48,12 @@ func TestAgentRuntimeFrameworkRequestSurvivesScheduledTrigger(t *testing.T) { defaults = []string{"nsa"} } args := map[string]interface{}{utils.KubescapeScanV1: payload} - requested, err := getKubescapeV1ScanRequest(args, defaults) + requested, err := getKubescapeV1ScanRequest(args, defaults, []string{"C-0069"}) require.NoError(t, err) scheduled, err := getKubescapeRequest(args, defaults) require.NoError(t, err) - require.Equal(t, requested, scheduled) + require.Equal(t, []string{"C-0069", "C-0070"}, requested.ExcludeControls) + require.Equal(t, []string{"C-0070"}, scheduled.ExcludeControls) assert.Equal(t, []string{"AgentRuntimeHardening"}, scheduled.TargetNames) assert.Equal(t, utilsapisv1.KindFramework, scheduled.TargetType) assert.Equal(t, []string{"agents"}, scheduled.IncludeNamespaces) @@ -98,7 +100,7 @@ func TestAgentRuntimeFrameworkRequestSurvivesScheduledTrigger(t *testing.T) { cfg, err := config.NewOperatorConfig( config.CapabilitiesConfig{Components: config.Components{Kubescape: config.Component{Enabled: true}, KubescapeScheduler: config.Component{Enabled: true}}}, utilsmetadata.ClusterConfig{KubescapeURL: strings.TrimPrefix(scanner.URL, "http://"), InstallationData: armotypes.InstallationData{DefaultFrameworks: defaults}}, - &beUtils.Credentials{}, config.Config{Namespace: "kubescape"}, + &beUtils.Credentials{}, config.Config{Namespace: "kubescape", ExcludeControls: []string{"C-0069"}}, ) require.NoError(t, err) args["jobParams"] = apis.CronJobParams{CronTabSchedule: "0 3 * * *"} @@ -124,9 +126,12 @@ func TestAgentRuntimeFrameworkRequestSurvivesScheduledTrigger(t *testing.T) { require.NoError(t, json.Unmarshal([]byte(configMap.Data["request-body.json"]), &commands)) require.Len(t, commands.Commands, 1) assert.Equal(t, apis.TypeRunKubescape, commands.Commands[0].CommandName) - replayed, err := getKubescapeV1ScanRequest(commands.Commands[0].Args, []string{"mitre"}) + replayed, err := getKubescapeV1ScanRequest(commands.Commands[0].Args, []string{"mitre"}, []string{"C-0069"}) require.NoError(t, err) - assert.Equal(t, scheduled, replayed, "the stored trigger must preserve the complete supported request") + assert.Equal(t, requested, replayed, "scheduled execution must reapply installation settings") + changed, err := getKubescapeV1ScanRequest(commands.Commands[0].Args, defaults, nil) + require.NoError(t, err) + assert.Equal(t, scheduled, changed, "removing installation exclusions must take effect on existing jobs") assert.Equal(t, "0 3 * * *", job.Spec.Schedule) assert.Equal(t, "AgentRuntimeHardening", job.Spec.JobTemplate.Spec.Template.Annotations["armo.framework"]) diff --git a/mainhandler/control_exclusions_dispatch_test.go b/mainhandler/control_exclusions_dispatch_test.go new file mode 100644 index 0000000..ebe39fa --- /dev/null +++ b/mainhandler/control_exclusions_dispatch_test.go @@ -0,0 +1,94 @@ +package mainhandler + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" + + "github.com/armosec/armoapi-go/apis" + "github.com/armosec/armoapi-go/armotypes" + utilsmetadata "github.com/armosec/utils-k8s-go/armometadata" + beUtils "github.com/kubescape/backend/pkg/utils" + utilsmetav1 "github.com/kubescape/opa-utils/httpserver/meta/v1" + "github.com/kubescape/operator/config" + "github.com/kubescape/operator/utils" + "github.com/stretchr/testify/require" +) + +func TestControlExclusionsScheduledReplayAndInvalidRequestsAtDispatch(t *testing.T) { + var dispatched atomic.Int32 + received := make(chan utilsmetav1.PostScanRequest, 8) + scanner := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + dispatched.Add(1) + var request utilsmetav1.PostScanRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + t.Errorf("decode scan: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + received <- request + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"independent-scan","type":"success"}`)) + })) + defer scanner.Close() + previousClient := KubescapeHttpClient + KubescapeHttpClient = scanner.Client() + t.Cleanup(func() { KubescapeHttpClient = previousClient }) + handler := func(args map[string]interface{}, globals []string) *ActionHandler { + t.Helper() + cfg, err := config.NewOperatorConfig( + config.CapabilitiesConfig{Components: config.Components{Kubescape: config.Component{Enabled: true}}}, + utilsmetadata.ClusterConfig{KubescapeURL: strings.TrimPrefix(scanner.URL, "http://"), InstallationData: armotypes.InstallationData{DefaultFrameworks: []string{"nsa"}}}, + &beUtils.Credentials{}, config.Config{Namespace: "kubescape", ExcludeControls: globals}, + ) + require.NoError(t, err) + return &ActionHandler{config: cfg, sessionObj: &utils.SessionObj{Command: &apis.Command{Args: args}}} + } + + // Use the same serialization path as scheduled jobs, then replay its stored command. + args := map[string]interface{}{utils.KubescapeScanV1: map[string]interface{}{"excludeControls": []string{"C-0070"}, "hostScanner": false}} + scheduled, err := getKubescapeRequest(args, []string{"nsa"}) + require.NoError(t, err) + stored, err := wrapRequestWithCommand(scheduled) + require.NoError(t, err) + var commands apis.Commands + require.NoError(t, json.Unmarshal(stored, &commands)) + require.Len(t, commands.Commands, 1) + for _, tt := range []struct { + name string + globals, want []string + }{ + {"original installation", []string{"C-0069"}, []string{"C-0069", "C-0070"}}, + {"changed installation", []string{"C-0071"}, []string{"C-0071", "C-0070"}}, + {"removed installation exclusions", nil, []string{"C-0070"}}, + } { + t.Run(tt.name, func(t *testing.T) { + before := dispatched.Load() + require.NoError(t, handler(commands.Commands[0].Args, tt.globals).kubescapeScan(context.Background())) + require.Equal(t, before+1, dispatched.Load()) + select { + case sent := <-received: + require.Equal(t, tt.want, sent.ExcludeControls, "stored schedule must use current installation settings at actual dispatch") + require.Equal(t, []string{"nsa"}, sent.TargetNames) + require.NotNil(t, sent.HostScanner) + require.False(t, *sent.HostScanner) + default: + t.Fatal("successful dispatch did not reach the scanner") + } + }) + } + for _, value := range []interface{}{"C-0069", []interface{}{42}, []interface{}{nil}, []string{""}, []string{" "}} { + data, err := json.Marshal(value) + require.NoError(t, err) + t.Run("invalid "+string(data), func(t *testing.T) { + before := dispatched.Load() + args := map[string]interface{}{utils.KubescapeScanV1: map[string]interface{}{"excludeControls": value}} + require.Error(t, handler(args, []string{"C-0069"}).kubescapeScan(context.Background())) + require.Equal(t, before, dispatched.Load(), "bad exclusions must not trigger an HTTP scan") + }) + } +} diff --git a/mainhandler/control_exclusions_test.go b/mainhandler/control_exclusions_test.go new file mode 100644 index 0000000..f7f1fff --- /dev/null +++ b/mainhandler/control_exclusions_test.go @@ -0,0 +1,109 @@ +package mainhandler + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/armosec/armoapi-go/apis" + utilsmetadata "github.com/armosec/utils-k8s-go/armometadata" + "github.com/kubescape/operator/config" + "github.com/kubescape/operator/continuousscanning" + "github.com/kubescape/operator/utils" + storagefake "github.com/kubescape/storage/pkg/generated/clientset/versioned/fake" + "github.com/panjf2000/ants/v2" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/watch" +) + +func TestOperatorControlExclusions(t *testing.T) { + args := map[string]interface{}{utils.KubescapeScanV1: map[string]interface{}{ + "targetType": "framework", "targetNames": []string{"nsa"}, + "excludeControls": []string{"C-0070"}, + }} + request, err := getKubescapeV1ScanRequest(args, nil, nil) + require.NoError(t, err) + body, err := json.Marshal(request) + require.NoError(t, err) + var fields map[string]json.RawMessage + require.NoError(t, json.Unmarshal(body, &fields)) + require.JSONEq(t, `["C-0070"]`, string(fields["excludeControls"])) +} + +func TestOperatorControlExclusionsDefaultsAndReplay(t *testing.T) { + defaults := []string{" C-0069 "} + tests := []struct { + name string + payload interface{} + want []string + wantError bool + }{ + {"absent", map[string]interface{}{}, []string{"C-0069"}, false}, + {"empty", map[string]interface{}{"excludeControls": []string{}}, []string{"C-0069"}, false}, + {"null", map[string]interface{}{"excludeControls": nil}, []string{"C-0069"}, false}, + {"union", map[string]interface{}{"excludeControls": []string{"c-0069", " C-0070 "}}, []string{"C-0069", "C-0070"}, false}, + {"blank", map[string]interface{}{"excludeControls": []string{" "}}, nil, true}, + {"wrong type", map[string]interface{}{"excludeControls": "C-0070"}, nil, true}, + {"null entry", map[string]interface{}{"excludeControls": []interface{}{nil}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := map[string]interface{}{utils.KubescapeScanV1: tt.payload} + request, err := getKubescapeV1ScanRequest(args, nil, defaults) + if tt.wantError { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, request.ExcludeControls) + data, err := wrapRequestWithCommand(request) + require.NoError(t, err) + var commands apis.Commands + require.NoError(t, json.Unmarshal(data, &commands)) + replayed, err := getKubescapeV1ScanRequest(commands.Commands[0].Args, nil, defaults) + require.NoError(t, err) + require.Equal(t, request, replayed) + request.ExcludeControls[0] = "changed" + require.Equal(t, []string{" C-0069 "}, defaults) + }) + } + _, err := getKubescapeV1ScanRequest(map[string]interface{}{utils.KubescapeScanV1: map[string]interface{}{}}, nil, []string{" "}) + require.Error(t, err) +} + +func TestContinuousControlExclusions(t *testing.T) { + cfg, err := config.NewOperatorConfig(config.CapabilitiesConfig{}, utilsmetadata.ClusterConfig{ClusterName: "test"}, nil, config.Config{ExcludeControls: []string{"C-0069"}}) + require.NoError(t, err) + jobs := make(chan utils.Job, 1) + pool, err := ants.NewPoolWithFunc(1, func(value interface{}) { jobs <- value.(utils.Job) }) + require.NoError(t, err) + defer pool.Release() + object := &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "v1", "kind": "Pod", "metadata": map[string]interface{}{"name": "example", "namespace": "default"}, + "spec": map[string]interface{}{"containers": []interface{}{map[string]interface{}{"name": "app", "image": "nginx"}}}, + }} + for _, eventType := range []watch.EventType{watch.Added, watch.Modified, watch.Deleted} { + t.Run(string(eventType), func(t *testing.T) { + handler := continuousscanning.NewTriggeringHandler(pool, cfg) + if eventType == watch.Deleted { + handler = continuousscanning.NewDeletedCleanerHandler(pool, cfg, storagefake.NewSimpleClientset()) + } + require.NoError(t, handler.Handle(context.Background(), watch.Event{Type: eventType, Object: object})) + select { + case job := <-jobs: + request, err := getKubescapeV1ScanRequest(job.Obj().Command.Args, cfg.DefaultFrameworks(), cfg.ExcludeControls()) + require.NoError(t, err) + require.Equal(t, []string{"C-0069"}, request.ExcludeControls) + require.NotNil(t, request.ScanObject) + require.NotNil(t, request.IsDeletedScanObject) + require.Equal(t, eventType == watch.Deleted, *request.IsDeletedScanObject) + require.NotNil(t, request.HostScanner) + require.False(t, *request.HostScanner) + case <-time.After(5 * time.Second): + t.Fatal("continuous event did not create a scan command") + } + }) + } +} diff --git a/mainhandler/kubescapehandler.go b/mainhandler/kubescapehandler.go index e67d4ed..ad750ff 100644 --- a/mainhandler/kubescapehandler.go +++ b/mainhandler/kubescapehandler.go @@ -162,7 +162,7 @@ func (actionHandler *ActionHandler) kubescapeScan(ctx context.Context) error { return errors.New("kubescape is not enabled") } - request, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, actionHandler.config.DefaultFrameworks()) + request, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, actionHandler.config.DefaultFrameworks(), actionHandler.config.ExcludeControls()) if err != nil { return err } diff --git a/mainhandler/kubescapehandlerhelper.go b/mainhandler/kubescapehandlerhelper.go index 10b59bd..f913096 100644 --- a/mainhandler/kubescapehandlerhelper.go +++ b/mainhandler/kubescapehandlerhelper.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "net/url" + "slices" "strings" "github.com/armosec/armoapi-go/apis" @@ -20,7 +21,6 @@ import ( v1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/strings/slices" ) const ( @@ -53,7 +53,7 @@ func getKubescapeV1ScanStatusURL(config config.IConfig, scanID string) *url.URL return &ksURL } -func getKubescapeV1ScanRequest(args map[string]interface{}, defaultFrameworks []string) (*utilsmetav1.PostScanRequest, error) { +func getKubescapeV1ScanRequest(args map[string]interface{}, defaultFrameworks, excludedControls []string) (*utilsmetav1.PostScanRequest, error) { scanV1, ok := args[utils.KubescapeScanV1] if !ok { @@ -70,6 +70,23 @@ func getKubescapeV1ScanRequest(args map[string]interface{}, defaultFrameworks [] return nil, fmt.Errorf("failed to convert request to v1/scan object, reason: %s", err.Error()) } + // Merge installation and request settings without mutating config or growing scheduled requests. + controls := append(slices.Clone(excludedControls), postScanRequest.ExcludeControls...) + postScanRequest.ExcludeControls = nil + seen := make(map[string]struct{}, len(controls)) + for _, control := range controls { + control = strings.TrimSpace(control) + if control == "" { + return nil, fmt.Errorf("excludeControls contains an empty control identifier") + } + key := strings.ToLower(control) + if _, exists := seen[key]; exists { + continue + } + seen[key] = struct{}{} + postScanRequest.ExcludeControls = append(postScanRequest.ExcludeControls, control) + } + // Drop blank entries so targetNames: [""] is treated as "no explicit target". names := make([]string, 0, len(postScanRequest.TargetNames)) for _, n := range postScanRequest.TargetNames { @@ -108,7 +125,8 @@ func readKubescapeV1ScanResponse(resp *http.Response) (*utilsmetav1.Response, er } func getKubescapeRequest(args map[string]interface{}, defaultFrameworks []string) (*utilsmetav1.PostScanRequest, error) { - postScanRequest, err := getKubescapeV1ScanRequest(args, defaultFrameworks) + // Keep installation settings out of stored jobs; apply current settings at execution. + postScanRequest, err := getKubescapeV1ScanRequest(args, defaultFrameworks, nil) if err != nil { return postScanRequest, err } diff --git a/mainhandler/kubescapehandlerhelper_test.go b/mainhandler/kubescapehandlerhelper_test.go index 9bebec4..b43942e 100644 --- a/mainhandler/kubescapehandlerhelper_test.go +++ b/mainhandler/kubescapehandlerhelper_test.go @@ -29,7 +29,7 @@ func TestGetKubescapeV1ScanRequest(t *testing.T) { }, }, } - req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, nil) + req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, nil, nil) assert.NoError(t, err) assert.NotNil(t, 0, req) } @@ -39,7 +39,7 @@ func TestGetKubescapeV1ScanRequest(t *testing.T) { Command: &apis.Command{Args: map[string]interface{}{utils.KubescapeScanV1: map[string]interface{}{"format": "json"}}}, }, } - req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, nil) + req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, nil, nil) assert.NoError(t, err) assert.Equal(t, "json", req.Format) } @@ -49,7 +49,7 @@ func TestGetKubescapeV1ScanRequest(t *testing.T) { Command: &apis.Command{Args: map[string]interface{}{utils.KubescapeScanV1: map[string]interface{}{}}}, }, } - req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, nil) + req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, nil, nil) assert.NoError(t, err) assert.Equal(t, "all", req.TargetNames[0]) assert.Equal(t, utilsapisv1.KindFramework, req.TargetType) @@ -61,7 +61,7 @@ func TestGetKubescapeV1ScanRequest(t *testing.T) { Command: &apis.Command{Args: map[string]interface{}{utils.KubescapeScanV1: map[string]interface{}{"targetType": utilsapisv1.KindFramework, "targetNames": []string{""}}}}, }, } - req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, nil) + req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, nil, nil) assert.NoError(t, err) assert.Equal(t, "all", req.TargetNames[0]) assert.Equal(t, utilsapisv1.KindFramework, req.TargetType) @@ -76,7 +76,7 @@ func TestGetKubescapeV1ScanRequest(t *testing.T) { }}}, }, } - req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, []string{"cis-eks-t1.2.0", "nsa"}) + req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, []string{"cis-eks-t1.2.0", "nsa"}, nil) assert.NoError(t, err) assert.Equal(t, []string{"cis-eks-t1.2.0", "nsa"}, req.TargetNames) assert.Equal(t, utilsapisv1.KindFramework, req.TargetType) @@ -87,7 +87,7 @@ func TestGetKubescapeV1ScanRequest(t *testing.T) { Command: &apis.Command{Args: map[string]interface{}{utils.KubescapeScanV1: map[string]interface{}{}}}, }, } - req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, []string{"cis-aks-t1.2.0"}) + req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, []string{"cis-aks-t1.2.0"}, nil) assert.NoError(t, err) assert.Equal(t, []string{"cis-aks-t1.2.0"}, req.TargetNames) assert.Equal(t, utilsapisv1.KindFramework, req.TargetType) @@ -101,7 +101,7 @@ func TestGetKubescapeV1ScanRequest(t *testing.T) { }}}, }, } - req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, []string{"cis-eks-t1.2.0"}) + req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, []string{"cis-eks-t1.2.0"}, nil) assert.NoError(t, err) assert.Equal(t, []string{"cis-eks-t1.2.0"}, req.TargetNames) assert.Equal(t, utilsapisv1.KindFramework, req.TargetType) @@ -116,7 +116,7 @@ func TestGetKubescapeV1ScanRequest(t *testing.T) { }}}, }, } - req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, []string{"cis-eks-t1.2.0"}) + req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, []string{"cis-eks-t1.2.0"}, nil) assert.NoError(t, err) assert.Equal(t, []string{"nsa"}, req.TargetNames) } @@ -130,7 +130,7 @@ func TestGetKubescapeV1ScanRequest(t *testing.T) { }}}, }, } - req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, []string{"cis-eks-t1.2.0"}) + req, err := getKubescapeV1ScanRequest(actionHandler.sessionObj.Command.Args, []string{"cis-eks-t1.2.0"}, nil) assert.NoError(t, err) assert.Empty(t, req.TargetNames) assert.Equal(t, utilsapisv1.KindControl, req.TargetType)