Skip to content

Commit 28a27f5

Browse files
authored
trigger AP scan in kubevuln whenever the application profile can be used (#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]>
1 parent 2b4bc6e commit 28a27f5

6 files changed

+240
-97
lines changed

mainhandler/handlerequests.go

+39-23
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ package mainhandler
33
import (
44
"context"
55
"fmt"
6-
exporters "github.com/kubescape/operator/admission/exporter"
76
"os"
87
"regexp"
98
"time"
109

10+
exporters "github.com/kubescape/operator/admission/exporter"
11+
1112
"github.com/kubescape/backend/pkg/versioncheck"
1213
"github.com/kubescape/k8s-interface/workloadinterface"
1314
core1 "k8s.io/api/core/v1"
@@ -426,7 +427,6 @@ func (mainHandler *MainHandler) HandleImageScanningScopedRequest(ctx context.Con
426427
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()))
427428
return nil
428429
}
429-
430430
for _, instanceID := range instanceIDs {
431431
s, _ := instanceID.GetSlug(false)
432432
if ok := slugs[s]; ok {
@@ -441,28 +441,44 @@ func (mainHandler *MainHandler) HandleImageScanningScopedRequest(ctx context.Con
441441
continue
442442
}
443443

444-
// set scanning command
445-
cmd := &apis.Command{
446-
Wlid: containerData.Wlid,
447-
CommandName: apis.TypeScanImages,
448-
Args: map[string]interface{}{
449-
utils.ArgsContainerData: containerData,
450-
utils.ArgsPod: pod,
451-
},
452-
}
453-
454-
// send specific command to the channel
455-
newSessionObj := utils.NewSessionObj(ctx, mainHandler.config, cmd, "Websocket", sessionObj.Reporter.GetJobID(), "", 1)
456-
457-
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))
458-
if err := mainHandler.HandleSingleRequest(ctx, newSessionObj); err != nil {
459-
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))
460-
newSessionObj.Reporter.SendError(err, mainHandler.sendReport, true)
461-
continue
444+
noContainerSlug, _ := instanceID.GetSlug(true)
445+
if appProfile := utils.GetApplicationProfileForRelevancyScan(ctx, mainHandler.ksStorageClient, noContainerSlug, ns); appProfile != nil {
446+
cmd := utils.GetApplicationProfileScanCommand(appProfile)
447+
448+
// send specific command to the channel
449+
newSessionObj := utils.NewSessionObj(ctx, mainHandler.config, cmd, "Websocket", sessionObj.Reporter.GetJobID(), "", 1)
450+
logger.L().Info("triggering application profile scan", helpers.String("wlid", cmd.Wlid), helpers.String("name", appProfile.Name), helpers.String("namespace", appProfile.Namespace))
451+
if err := mainHandler.HandleSingleRequest(ctx, newSessionObj); err != nil {
452+
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))
453+
newSessionObj.Reporter.SendError(err, mainHandler.sendReport, true)
454+
continue
455+
}
456+
newSessionObj.Reporter.SendStatus(systemreports.JobDone, mainHandler.sendReport)
457+
logger.L().Info("action completed successfully", helpers.String("name", appProfile.Name), helpers.String("namespace", appProfile.Namespace))
458+
slugs[noContainerSlug] = true
459+
} else {
460+
// set scanning command
461+
cmd := &apis.Command{
462+
Wlid: containerData.Wlid,
463+
CommandName: apis.TypeScanImages,
464+
Args: map[string]interface{}{
465+
utils.ArgsContainerData: containerData,
466+
utils.ArgsPod: pod,
467+
},
468+
}
469+
// send specific command to the channel
470+
newSessionObj := utils.NewSessionObj(ctx, mainHandler.config, cmd, "Websocket", sessionObj.Reporter.GetJobID(), "", 1)
471+
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))
472+
473+
if err := mainHandler.HandleSingleRequest(ctx, newSessionObj); err != nil {
474+
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))
475+
newSessionObj.Reporter.SendError(err, mainHandler.sendReport, true)
476+
continue
477+
}
478+
newSessionObj.Reporter.SendStatus(systemreports.JobDone, mainHandler.sendReport)
479+
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))
480+
slugs[s] = true
462481
}
463-
newSessionObj.Reporter.SendStatus(systemreports.JobDone, mainHandler.sendReport)
464-
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))
465-
slugs[s] = true
466482
}
467483
return nil
468484
}); err != nil {

utils/applicationprofile.go

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package utils
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"slices"
7+
8+
"github.com/armosec/armoapi-go/apis"
9+
"github.com/kubescape/go-logger"
10+
"github.com/kubescape/go-logger/helpers"
11+
helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers"
12+
"github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1"
13+
kssc "github.com/kubescape/storage/pkg/generated/clientset/versioned"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
)
16+
17+
func SkipApplicationProfile(annotations map[string]string) (bool, error) {
18+
ann := []string{
19+
"", // empty string for backward compatibility
20+
helpersv1.Ready,
21+
helpersv1.Completed,
22+
}
23+
24+
if len(annotations) == 0 {
25+
return true, fmt.Errorf("no annotations") // skip
26+
}
27+
28+
if status, ok := annotations[helpersv1.StatusMetadataKey]; ok && !slices.Contains(ann, status) {
29+
return true, fmt.Errorf("invalid status")
30+
}
31+
if val, ok := annotations[helpersv1.InstanceIDMetadataKey]; !ok || val == "" {
32+
return true, fmt.Errorf("missing InstanceID annotation") // skip
33+
}
34+
if val, ok := annotations[helpersv1.WlidMetadataKey]; !ok || val == "" {
35+
return true, fmt.Errorf("missing WLID annotation") // skip
36+
}
37+
38+
return false, nil // do not skip
39+
}
40+
41+
// GetApplicationProfileForRelevancyScan retrieves an application profile from the storage client based on the provided slug and namespace
42+
// 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
43+
func GetApplicationProfileForRelevancyScan(ctx context.Context, storageClient kssc.Interface, slug, namespace string) *v1beta1.ApplicationProfile {
44+
appProfile, err := storageClient.SpdxV1beta1().ApplicationProfiles(namespace).Get(ctx, slug, metav1.GetOptions{ResourceVersion: "metadata"})
45+
if err == nil && appProfile != nil {
46+
if skip, err := SkipApplicationProfile(appProfile.Annotations); skip {
47+
logger.L().Info("found application profile, but skipping", helpers.Error(err), helpers.String("id", slug), helpers.String("namespace", namespace),
48+
helpers.Interface("annotations", appProfile.Annotations))
49+
return nil
50+
} else {
51+
logger.L().Info("found application profile", helpers.String("id", slug), helpers.String("namespace", namespace))
52+
return appProfile
53+
}
54+
} else {
55+
logger.L().Info("application profile not found", helpers.String("id", slug), helpers.String("namespace", namespace))
56+
}
57+
return nil
58+
}
59+
60+
func GetApplicationProfileScanCommand(appProfile *v1beta1.ApplicationProfile) *apis.Command {
61+
return &apis.Command{
62+
Wlid: appProfile.Annotations[helpersv1.WlidMetadataKey],
63+
CommandName: apis.TypeScanApplicationProfile,
64+
Args: map[string]interface{}{
65+
ArgsName: appProfile.Name,
66+
ArgsNamespace: appProfile.Namespace,
67+
},
68+
}
69+
}

utils/applicationprofile_test.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package utils
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestSkipApplicationProfile(t *testing.T) {
12+
tests := []struct {
13+
annotations map[string]string
14+
name string
15+
wantSkip bool
16+
expectedErr error
17+
}{
18+
{
19+
name: "status is empty",
20+
annotations: map[string]string{
21+
helpersv1.StatusMetadataKey: "",
22+
helpersv1.WlidMetadataKey: "wlid",
23+
helpersv1.InstanceIDMetadataKey: "instanceID",
24+
},
25+
wantSkip: false,
26+
},
27+
{
28+
name: "status is Ready",
29+
annotations: map[string]string{
30+
helpersv1.StatusMetadataKey: helpersv1.Ready,
31+
helpersv1.WlidMetadataKey: "wlid",
32+
helpersv1.InstanceIDMetadataKey: "instanceID",
33+
},
34+
wantSkip: false,
35+
},
36+
{
37+
name: "status is Completed",
38+
annotations: map[string]string{
39+
helpersv1.StatusMetadataKey: helpersv1.Completed,
40+
helpersv1.WlidMetadataKey: "wlid",
41+
helpersv1.InstanceIDMetadataKey: "instanceID",
42+
},
43+
wantSkip: false,
44+
},
45+
{
46+
name: "status is not recognized",
47+
annotations: map[string]string{
48+
helpersv1.StatusMetadataKey: "NotRecognized",
49+
},
50+
wantSkip: true,
51+
expectedErr: fmt.Errorf("invalid status"),
52+
},
53+
{
54+
name: "no status annotation",
55+
annotations: map[string]string{},
56+
wantSkip: true,
57+
expectedErr: fmt.Errorf("no annotations"),
58+
},
59+
{
60+
name: "missing instance WLID annotation",
61+
annotations: map[string]string{
62+
helpersv1.StatusMetadataKey: helpersv1.Ready,
63+
helpersv1.InstanceIDMetadataKey: "instanceID",
64+
},
65+
wantSkip: true,
66+
expectedErr: fmt.Errorf("missing WLID annotation"),
67+
},
68+
69+
{
70+
name: "missing instance ID annotation",
71+
annotations: map[string]string{
72+
helpersv1.StatusMetadataKey: helpersv1.Ready,
73+
helpersv1.WlidMetadataKey: "wlid",
74+
},
75+
wantSkip: true,
76+
expectedErr: fmt.Errorf("missing InstanceID annotation"),
77+
},
78+
}
79+
80+
for _, tt := range tests {
81+
t.Run(tt.name, func(t *testing.T) {
82+
gotSkip, err := SkipApplicationProfile(tt.annotations)
83+
assert.Equal(t, tt.wantSkip, gotSkip)
84+
assert.Equal(t, tt.expectedErr, err)
85+
})
86+
}
87+
}

watcher/applicationprofilewatcher.go

+3-21
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package watcher
33
import (
44
"context"
55
"fmt"
6-
"slices"
76
"time"
87

98
spdxv1beta1 "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1"
@@ -83,7 +82,7 @@ func (wh *WatchHandler) HandleApplicationProfileEvents(sfEvents <-chan watch.Eve
8382
defer close(errorCh)
8483

8584
for e := range sfEvents {
86-
logger.L().Info("Matthias received application profile event", helpers.Interface("event", e))
85+
logger.L().Info("received application profile event", helpers.Interface("event", e))
8786
obj, ok := e.Object.(*spdxv1beta1.ApplicationProfile)
8887
if !ok {
8988
errorCh <- ErrUnsupportedObject
@@ -101,7 +100,7 @@ func (wh *WatchHandler) HandleApplicationProfileEvents(sfEvents <-chan watch.Eve
101100
continue
102101
}
103102

104-
if skipAP(obj.ObjectMeta.Annotations) {
103+
if skip, _ := utils.SkipApplicationProfile(obj.ObjectMeta.Annotations); skip {
105104
continue
106105
}
107106

@@ -117,28 +116,11 @@ func (wh *WatchHandler) HandleApplicationProfileEvents(sfEvents <-chan watch.Eve
117116
},
118117
}
119118
// send command
120-
logger.L().Info("Matthias scanning application profile", helpers.String("wlid", cmd.Wlid), helpers.String("name", obj.Name), helpers.String("namespace", obj.Namespace))
119+
logger.L().Info("scanning application profile", helpers.String("wlid", cmd.Wlid), helpers.String("name", obj.Name), helpers.String("namespace", obj.Namespace))
121120
producedCommands <- cmd
122121
}
123122
}
124123

125-
func skipAP(annotations map[string]string) bool {
126-
ann := []string{
127-
"", // empty string for backward compatibility
128-
helpersv1.Ready,
129-
helpersv1.Completed,
130-
}
131-
132-
if len(annotations) == 0 {
133-
return true // skip
134-
}
135-
136-
if status, ok := annotations[helpersv1.StatusMetadataKey]; ok {
137-
return !slices.Contains(ann, status)
138-
}
139-
return false // do not skip
140-
}
141-
142124
func (wh *WatchHandler) getApplicationProfileWatcher() (watch.Interface, error) {
143125
// no need to support ExcludeNamespaces and IncludeNamespaces since node-agent will respect them as well
144126
return wh.storageClient.SpdxV1beta1().ApplicationProfiles("").Watch(context.Background(), v1.ListOptions{})

watcher/applicationprofilewatcher_test.go

-49
Original file line numberDiff line numberDiff line change
@@ -198,52 +198,3 @@ func TestHandleApplicationProfileEvents(t *testing.T) {
198198

199199
}
200200
}
201-
202-
func TestSkipAP(t *testing.T) {
203-
tests := []struct {
204-
annotations map[string]string
205-
name string
206-
wantSkip bool
207-
}{
208-
{
209-
name: "status is empty",
210-
annotations: map[string]string{
211-
helpersv1.StatusMetadataKey: "",
212-
},
213-
wantSkip: false,
214-
},
215-
{
216-
name: "status is Ready",
217-
annotations: map[string]string{
218-
helpersv1.StatusMetadataKey: helpersv1.Ready,
219-
},
220-
wantSkip: false,
221-
},
222-
{
223-
name: "status is Completed",
224-
annotations: map[string]string{
225-
helpersv1.StatusMetadataKey: helpersv1.Completed,
226-
},
227-
wantSkip: false,
228-
},
229-
{
230-
name: "status is not recognized",
231-
annotations: map[string]string{
232-
helpersv1.StatusMetadataKey: "NotRecognized",
233-
},
234-
wantSkip: true,
235-
},
236-
{
237-
name: "no status annotation",
238-
annotations: map[string]string{},
239-
wantSkip: true,
240-
},
241-
}
242-
243-
for _, tt := range tests {
244-
t.Run(tt.name, func(t *testing.T) {
245-
gotSkip := skipAP(tt.annotations)
246-
assert.Equal(t, tt.wantSkip, gotSkip)
247-
})
248-
}
249-
}

0 commit comments

Comments
 (0)