diff --git a/mainhandler/handlerequests.go b/mainhandler/handlerequests.go index 26e268f..1762499 100644 --- a/mainhandler/handlerequests.go +++ b/mainhandler/handlerequests.go @@ -3,11 +3,12 @@ package mainhandler import ( "context" "fmt" - exporters "github.com/kubescape/operator/admission/exporter" "os" "regexp" "time" + exporters "github.com/kubescape/operator/admission/exporter" + "github.com/kubescape/backend/pkg/versioncheck" "github.com/kubescape/k8s-interface/workloadinterface" core1 "k8s.io/api/core/v1" @@ -426,7 +427,6 @@ func (mainHandler *MainHandler) HandleImageScanningScopedRequest(ctx context.Con logger.L().Debug("naked pod younger than guard time detected, skipping scan", helpers.String("pod", pod.GetName()), helpers.String("namespace", pod.GetNamespace()), helpers.String("creationTimestamp", pod.CreationTimestamp.String())) return nil } - for _, instanceID := range instanceIDs { s, _ := instanceID.GetSlug(false) if ok := slugs[s]; ok { @@ -441,28 +441,44 @@ func (mainHandler *MainHandler) HandleImageScanningScopedRequest(ctx context.Con continue } - // set scanning command - cmd := &apis.Command{ - Wlid: containerData.Wlid, - CommandName: apis.TypeScanImages, - Args: map[string]interface{}{ - utils.ArgsContainerData: containerData, - utils.ArgsPod: pod, - }, - } - - // send specific command to the channel - newSessionObj := utils.NewSessionObj(ctx, mainHandler.config, cmd, "Websocket", sessionObj.Reporter.GetJobID(), "", 1) - - logger.L().Info("triggering scan image", helpers.String("id", newSessionObj.Command.GetID()), helpers.String("slug", s), helpers.String("containerName", containerData.ContainerName), helpers.String("imageTag", containerData.ImageTag), helpers.String("imageID", containerData.ImageID)) - if err := mainHandler.HandleSingleRequest(ctx, newSessionObj); err != nil { - logger.L().Info("failed to complete action", helpers.Error(err), helpers.String("id", newSessionObj.Command.GetID()), helpers.String("slug", s), helpers.String("containerName", containerData.ContainerName), helpers.String("imageTag", containerData.ImageTag), helpers.String("imageID", containerData.ImageID)) - newSessionObj.Reporter.SendError(err, mainHandler.sendReport, true) - continue + noContainerSlug, _ := instanceID.GetSlug(true) + if appProfile := utils.GetApplicationProfileForRelevancyScan(ctx, mainHandler.ksStorageClient, noContainerSlug, ns); appProfile != nil { + cmd := utils.GetApplicationProfileScanCommand(appProfile) + + // send specific command to the channel + newSessionObj := utils.NewSessionObj(ctx, mainHandler.config, cmd, "Websocket", sessionObj.Reporter.GetJobID(), "", 1) + logger.L().Info("triggering application profile scan", helpers.String("wlid", cmd.Wlid), helpers.String("name", appProfile.Name), helpers.String("namespace", appProfile.Namespace)) + if err := mainHandler.HandleSingleRequest(ctx, newSessionObj); err != nil { + logger.L().Info("failed to complete action", helpers.Error(err), helpers.String("id", newSessionObj.Command.GetID()), helpers.String("name", appProfile.Name), helpers.String("namespace", appProfile.Namespace)) + newSessionObj.Reporter.SendError(err, mainHandler.sendReport, true) + continue + } + newSessionObj.Reporter.SendStatus(systemreports.JobDone, mainHandler.sendReport) + logger.L().Info("action completed successfully", helpers.String("name", appProfile.Name), helpers.String("namespace", appProfile.Namespace)) + slugs[noContainerSlug] = true + } else { + // set scanning command + cmd := &apis.Command{ + Wlid: containerData.Wlid, + CommandName: apis.TypeScanImages, + Args: map[string]interface{}{ + utils.ArgsContainerData: containerData, + utils.ArgsPod: pod, + }, + } + // send specific command to the channel + newSessionObj := utils.NewSessionObj(ctx, mainHandler.config, cmd, "Websocket", sessionObj.Reporter.GetJobID(), "", 1) + logger.L().Info("triggering scan image", helpers.String("id", newSessionObj.Command.GetID()), helpers.String("slug", s), helpers.String("containerName", containerData.ContainerName), helpers.String("imageTag", containerData.ImageTag), helpers.String("imageID", containerData.ImageID)) + + if err := mainHandler.HandleSingleRequest(ctx, newSessionObj); err != nil { + logger.L().Info("failed to complete action", helpers.Error(err), helpers.String("id", newSessionObj.Command.GetID()), helpers.String("slug", s), helpers.String("containerName", containerData.ContainerName), helpers.String("imageTag", containerData.ImageTag), helpers.String("imageID", containerData.ImageID)) + newSessionObj.Reporter.SendError(err, mainHandler.sendReport, true) + continue + } + newSessionObj.Reporter.SendStatus(systemreports.JobDone, mainHandler.sendReport) + logger.L().Info("action completed successfully", helpers.String("id", newSessionObj.Command.GetID()), helpers.String("slug", s), helpers.String("containerName", containerData.ContainerName), helpers.String("imageTag", containerData.ImageTag), helpers.String("imageID", containerData.ImageID)) + slugs[s] = true } - newSessionObj.Reporter.SendStatus(systemreports.JobDone, mainHandler.sendReport) - logger.L().Info("action completed successfully", helpers.String("id", newSessionObj.Command.GetID()), helpers.String("slug", s), helpers.String("containerName", containerData.ContainerName), helpers.String("imageTag", containerData.ImageTag), helpers.String("imageID", containerData.ImageID)) - slugs[s] = true } return nil }); err != nil { diff --git a/utils/applicationprofile.go b/utils/applicationprofile.go new file mode 100644 index 0000000..5e6a3a4 --- /dev/null +++ b/utils/applicationprofile.go @@ -0,0 +1,69 @@ +package utils + +import ( + "context" + "fmt" + "slices" + + "github.com/armosec/armoapi-go/apis" + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" + helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + kssc "github.com/kubescape/storage/pkg/generated/clientset/versioned" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func SkipApplicationProfile(annotations map[string]string) (bool, error) { + ann := []string{ + "", // empty string for backward compatibility + helpersv1.Ready, + helpersv1.Completed, + } + + if len(annotations) == 0 { + return true, fmt.Errorf("no annotations") // skip + } + + if status, ok := annotations[helpersv1.StatusMetadataKey]; ok && !slices.Contains(ann, status) { + return true, fmt.Errorf("invalid status") + } + if val, ok := annotations[helpersv1.InstanceIDMetadataKey]; !ok || val == "" { + return true, fmt.Errorf("missing InstanceID annotation") // skip + } + if val, ok := annotations[helpersv1.WlidMetadataKey]; !ok || val == "" { + return true, fmt.Errorf("missing WLID annotation") // skip + } + + return false, nil // do not skip +} + +// GetApplicationProfileForRelevancyScan retrieves an application profile from the storage client based on the provided slug and namespace +// If the application profile is found, and it should not be skipped (i.e. correct status, InstanceID and WLID annotations), it is returned, otherwise nil +func GetApplicationProfileForRelevancyScan(ctx context.Context, storageClient kssc.Interface, slug, namespace string) *v1beta1.ApplicationProfile { + appProfile, err := storageClient.SpdxV1beta1().ApplicationProfiles(namespace).Get(ctx, slug, metav1.GetOptions{ResourceVersion: "metadata"}) + if err == nil && appProfile != nil { + if skip, err := SkipApplicationProfile(appProfile.Annotations); skip { + logger.L().Info("found application profile, but skipping", helpers.Error(err), helpers.String("id", slug), helpers.String("namespace", namespace), + helpers.Interface("annotations", appProfile.Annotations)) + return nil + } else { + logger.L().Info("found application profile", helpers.String("id", slug), helpers.String("namespace", namespace)) + return appProfile + } + } else { + logger.L().Info("application profile not found", helpers.String("id", slug), helpers.String("namespace", namespace)) + } + return nil +} + +func GetApplicationProfileScanCommand(appProfile *v1beta1.ApplicationProfile) *apis.Command { + return &apis.Command{ + Wlid: appProfile.Annotations[helpersv1.WlidMetadataKey], + CommandName: apis.TypeScanApplicationProfile, + Args: map[string]interface{}{ + ArgsName: appProfile.Name, + ArgsNamespace: appProfile.Namespace, + }, + } +} diff --git a/utils/applicationprofile_test.go b/utils/applicationprofile_test.go new file mode 100644 index 0000000..75d64bf --- /dev/null +++ b/utils/applicationprofile_test.go @@ -0,0 +1,87 @@ +package utils + +import ( + "fmt" + "testing" + + helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" + "github.com/stretchr/testify/assert" +) + +func TestSkipApplicationProfile(t *testing.T) { + tests := []struct { + annotations map[string]string + name string + wantSkip bool + expectedErr error + }{ + { + name: "status is empty", + annotations: map[string]string{ + helpersv1.StatusMetadataKey: "", + helpersv1.WlidMetadataKey: "wlid", + helpersv1.InstanceIDMetadataKey: "instanceID", + }, + wantSkip: false, + }, + { + name: "status is Ready", + annotations: map[string]string{ + helpersv1.StatusMetadataKey: helpersv1.Ready, + helpersv1.WlidMetadataKey: "wlid", + helpersv1.InstanceIDMetadataKey: "instanceID", + }, + wantSkip: false, + }, + { + name: "status is Completed", + annotations: map[string]string{ + helpersv1.StatusMetadataKey: helpersv1.Completed, + helpersv1.WlidMetadataKey: "wlid", + helpersv1.InstanceIDMetadataKey: "instanceID", + }, + wantSkip: false, + }, + { + name: "status is not recognized", + annotations: map[string]string{ + helpersv1.StatusMetadataKey: "NotRecognized", + }, + wantSkip: true, + expectedErr: fmt.Errorf("invalid status"), + }, + { + name: "no status annotation", + annotations: map[string]string{}, + wantSkip: true, + expectedErr: fmt.Errorf("no annotations"), + }, + { + name: "missing instance WLID annotation", + annotations: map[string]string{ + helpersv1.StatusMetadataKey: helpersv1.Ready, + helpersv1.InstanceIDMetadataKey: "instanceID", + }, + wantSkip: true, + expectedErr: fmt.Errorf("missing WLID annotation"), + }, + + { + name: "missing instance ID annotation", + annotations: map[string]string{ + helpersv1.StatusMetadataKey: helpersv1.Ready, + helpersv1.WlidMetadataKey: "wlid", + }, + wantSkip: true, + expectedErr: fmt.Errorf("missing InstanceID annotation"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotSkip, err := SkipApplicationProfile(tt.annotations) + assert.Equal(t, tt.wantSkip, gotSkip) + assert.Equal(t, tt.expectedErr, err) + }) + } +} diff --git a/watcher/applicationprofilewatcher.go b/watcher/applicationprofilewatcher.go index fb2fd1b..b3c5f06 100644 --- a/watcher/applicationprofilewatcher.go +++ b/watcher/applicationprofilewatcher.go @@ -3,7 +3,6 @@ package watcher import ( "context" "fmt" - "slices" "time" spdxv1beta1 "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" @@ -83,7 +82,7 @@ func (wh *WatchHandler) HandleApplicationProfileEvents(sfEvents <-chan watch.Eve defer close(errorCh) for e := range sfEvents { - logger.L().Info("Matthias received application profile event", helpers.Interface("event", e)) + logger.L().Info("received application profile event", helpers.Interface("event", e)) obj, ok := e.Object.(*spdxv1beta1.ApplicationProfile) if !ok { errorCh <- ErrUnsupportedObject @@ -101,7 +100,7 @@ func (wh *WatchHandler) HandleApplicationProfileEvents(sfEvents <-chan watch.Eve continue } - if skipAP(obj.ObjectMeta.Annotations) { + if skip, _ := utils.SkipApplicationProfile(obj.ObjectMeta.Annotations); skip { continue } @@ -117,28 +116,11 @@ func (wh *WatchHandler) HandleApplicationProfileEvents(sfEvents <-chan watch.Eve }, } // send command - logger.L().Info("Matthias scanning application profile", helpers.String("wlid", cmd.Wlid), helpers.String("name", obj.Name), helpers.String("namespace", obj.Namespace)) + logger.L().Info("scanning application profile", helpers.String("wlid", cmd.Wlid), helpers.String("name", obj.Name), helpers.String("namespace", obj.Namespace)) producedCommands <- cmd } } -func skipAP(annotations map[string]string) bool { - ann := []string{ - "", // empty string for backward compatibility - helpersv1.Ready, - helpersv1.Completed, - } - - if len(annotations) == 0 { - return true // skip - } - - if status, ok := annotations[helpersv1.StatusMetadataKey]; ok { - return !slices.Contains(ann, status) - } - return false // do not skip -} - func (wh *WatchHandler) getApplicationProfileWatcher() (watch.Interface, error) { // no need to support ExcludeNamespaces and IncludeNamespaces since node-agent will respect them as well return wh.storageClient.SpdxV1beta1().ApplicationProfiles("").Watch(context.Background(), v1.ListOptions{}) diff --git a/watcher/applicationprofilewatcher_test.go b/watcher/applicationprofilewatcher_test.go index 6b954fc..7230093 100644 --- a/watcher/applicationprofilewatcher_test.go +++ b/watcher/applicationprofilewatcher_test.go @@ -198,52 +198,3 @@ func TestHandleApplicationProfileEvents(t *testing.T) { } } - -func TestSkipAP(t *testing.T) { - tests := []struct { - annotations map[string]string - name string - wantSkip bool - }{ - { - name: "status is empty", - annotations: map[string]string{ - helpersv1.StatusMetadataKey: "", - }, - wantSkip: false, - }, - { - name: "status is Ready", - annotations: map[string]string{ - helpersv1.StatusMetadataKey: helpersv1.Ready, - }, - wantSkip: false, - }, - { - name: "status is Completed", - annotations: map[string]string{ - helpersv1.StatusMetadataKey: helpersv1.Completed, - }, - wantSkip: false, - }, - { - name: "status is not recognized", - annotations: map[string]string{ - helpersv1.StatusMetadataKey: "NotRecognized", - }, - wantSkip: true, - }, - { - name: "no status annotation", - annotations: map[string]string{}, - wantSkip: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotSkip := skipAP(tt.annotations) - assert.Equal(t, tt.wantSkip, gotSkip) - }) - } -} diff --git a/watcher/podwatcher.go b/watcher/podwatcher.go index 2cf2149..470ee86 100644 --- a/watcher/podwatcher.go +++ b/watcher/podwatcher.go @@ -15,6 +15,7 @@ import ( instanceidhandlerv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1" "github.com/kubescape/k8s-interface/k8sinterface" "github.com/kubescape/operator/utils" + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" "github.com/panjf2000/ants/v2" core1 "k8s.io/api/core/v1" @@ -94,6 +95,8 @@ func (wh *WatchHandler) handlePodWatcher(ctx context.Context, pod *core1.Pod, wl return } + noContainerSlugs := map[string]bool{} + // there are a few use-cases: // 1. new workload, new image - new wlid, new slug, new image // scan // 2. new workload, existing image - new wlid, new slug, existing image // scan @@ -118,7 +121,20 @@ func (wh *WatchHandler) handlePodWatcher(ctx context.Context, pod *core1.Pod, wl continue } - wh.scanImage(ctx, pod, containerData, workerPool) + noContainerSlug, _ := slugToInstanceID[slug].GetSlug(true) + if _, ok := noContainerSlugs[noContainerSlug]; ok { + // already scanned the application profile + wh.SlugToImageID.Set(containerData.Slug, containerData.ImageID) + wh.WlidAndImageID.Add(getWlidAndImageID(containerData)) + continue + } + + if appProfile := utils.GetApplicationProfileForRelevancyScan(ctx, wh.storageClient, noContainerSlug, pod.GetNamespace()); appProfile != nil { + wh.scanApplicationProfile(ctx, appProfile, workerPool) + noContainerSlugs[noContainerSlug] = true + } else { + wh.scanImage(ctx, pod, containerData, workerPool) + } wh.SlugToImageID.Set(containerData.Slug, containerData.ImageID) wh.WlidAndImageID.Add(getWlidAndImageID(containerData)) @@ -142,11 +158,22 @@ func (wh *WatchHandler) handlePodWatcher(ctx context.Context, pod *core1.Pod, wl continue } - // use-case 1, 2, 3 - // scan image - wh.scanImage(ctx, pod, containerData, workerPool) + noContainerSlug, _ := slugToInstanceID[slug].GetSlug(true) + if _, ok := noContainerSlugs[noContainerSlug]; ok { + // already scanned the application profile + wh.WlidAndImageID.Add(getWlidAndImageID(containerData)) + continue + } + // use-case 1, 2, 3 + if appProfile := utils.GetApplicationProfileForRelevancyScan(ctx, wh.storageClient, noContainerSlug, pod.GetNamespace()); appProfile != nil { + wh.scanApplicationProfile(ctx, appProfile, workerPool) + noContainerSlugs[noContainerSlug] = true + } else { + wh.scanImage(ctx, pod, containerData, workerPool) + } wh.WlidAndImageID.Add(getWlidAndImageID(containerData)) + } } @@ -170,6 +197,17 @@ func (wh *WatchHandler) scanImage(ctx context.Context, pod *core1.Pod, container } } +func (wh *WatchHandler) scanApplicationProfile(ctx context.Context, appProfile *v1beta1.ApplicationProfile, workerPool *ants.PoolWithFunc) { + // set scanning command + cmd := utils.GetApplicationProfileScanCommand(appProfile) + + // send + logger.L().Info("scanning application profile", helpers.String("wlid", cmd.Wlid), helpers.String("name", appProfile.Name), helpers.String("namespace", appProfile.Namespace)) + if err := utils.AddCommandToChannel(ctx, wh.cfg, cmd, workerPool); err != nil { + logger.L().Ctx(ctx).Error("failed to add command to channel", helpers.Error(err), helpers.String("wlid", cmd.Wlid), helpers.String("name", appProfile.Name), helpers.String("namespace", appProfile.Namespace)) + } +} + func (wh *WatchHandler) listPods(ctx context.Context) error { if err := pager.New(func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) { return wh.k8sAPI.KubernetesClient.CoreV1().Pods("").List(ctx, opts)