Skip to content

Commit

Permalink
trigger AP scan in kubevuln whenever the application profile can be u…
Browse files Browse the repository at this point in the history
…sed (#277)

* trigger AP scan in kubevuln whenever the application profile can be used

Signed-off-by: Amir Malka <[email protected]>

* fix tests

Signed-off-by: Amir Malka <[email protected]>

---------

Signed-off-by: Amir Malka <[email protected]>
  • Loading branch information
amirmalka authored Dec 24, 2024
1 parent 2b4bc6e commit 28a27f5
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 97 deletions.
62 changes: 39 additions & 23 deletions mainhandler/handlerequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
69 changes: 69 additions & 0 deletions utils/applicationprofile.go
Original file line number Diff line number Diff line change
@@ -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,
},
}
}
87 changes: 87 additions & 0 deletions utils/applicationprofile_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
24 changes: 3 additions & 21 deletions watcher/applicationprofilewatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package watcher
import (
"context"
"fmt"
"slices"
"time"

spdxv1beta1 "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1"
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand All @@ -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{})
Expand Down
49 changes: 0 additions & 49 deletions watcher/applicationprofilewatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
Loading

0 comments on commit 28a27f5

Please sign in to comment.