Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
golang 1.25.6
41 changes: 27 additions & 14 deletions adapters/v1/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
cs "github.com/armosec/armoapi-go/containerscan"
v1 "github.com/armosec/armoapi-go/containerscan/v1"
"github.com/armosec/armoapi-go/identifiers"
"github.com/armosec/armoapi-go/scanfailure"
"github.com/armosec/utils-go/httputils"
pkgcautils "github.com/armosec/utils-k8s-go/armometadata"
wlidpkg "github.com/armosec/utils-k8s-go/wlid"
Expand All @@ -27,35 +28,36 @@ import (
"github.com/kubescape/k8s-interface/k8sinterface"
"github.com/kubescape/kubevuln/core/domain"
"github.com/kubescape/kubevuln/core/ports"
"github.com/armosec/armoapi-go/scanfailure"
"go.opentelemetry.io/otel"
)

type BackendAdapter struct {
eventReceiverRestURL string
apiServerRestURL string
clusterConfig pkgcautils.ClusterConfig
getCVEExceptionsFunc func(string, string, *identifiers.PortalDesignator, map[string]string) ([]armotypes.VulnerabilityExceptionPolicy, error)
httpPostFunc func(httputils.IHttpClient, string, map[string]string, []byte, time.Duration) (*http.Response, error)
sendStatusFunc func(*backendClientV1.BaseReportSender, string, bool)
accessKey string
eventReceiverRestURL string
apiServerRestURL string
clusterConfig pkgcautils.ClusterConfig
getCVEExceptionsFunc func(string, string, *identifiers.PortalDesignator, map[string]string) ([]armotypes.VulnerabilityExceptionPolicy, error)
httpPostFunc func(httputils.IHttpClient, string, map[string]string, []byte, time.Duration) (*http.Response, error)
sendStatusFunc func(*backendClientV1.BaseReportSender, string, bool)
accessKey string
securityExceptionRepo ports.SecurityExceptionRepository
}

var _ ports.Platform = (*BackendAdapter)(nil)

func NewBackendAdapter(accountID, apiServerRestURL, eventReceiverRestURL, accessKey string) *BackendAdapter {
func NewBackendAdapter(accountID, apiServerRestURL, eventReceiverRestURL, accessKey string, seRepo ports.SecurityExceptionRepository) *BackendAdapter {
return &BackendAdapter{
clusterConfig: pkgcautils.ClusterConfig{
AccountID: accountID,
},
eventReceiverRestURL: eventReceiverRestURL,
apiServerRestURL: apiServerRestURL,
getCVEExceptionsFunc: backendClientV1.GetCVEExceptionByDesignator,
httpPostFunc: httputils.HttpPostWithRetry,
eventReceiverRestURL: eventReceiverRestURL,
apiServerRestURL: apiServerRestURL,
getCVEExceptionsFunc: backendClientV1.GetCVEExceptionByDesignator,
httpPostFunc: httputils.HttpPostWithRetry,
sendStatusFunc: func(sender *backendClientV1.BaseReportSender, status string, sendReport bool) {
sender.SendStatus(status, sendReport) // TODO - update this function to use from kubescape/backend
},
accessKey: accessKey,
accessKey: accessKey,
securityExceptionRepo: seRepo,
}
}

Expand Down Expand Up @@ -102,6 +104,17 @@ func (a *BackendAdapter) GetCVEExceptions(ctx context.Context) (domain.CVEExcept
if err != nil {
return nil, err
}

// Merge CRD-based exceptions
namespace := wlidpkg.GetNamespaceFromWlid(workload.Wlid)
seList, cseList, err := a.securityExceptionRepo.GetSecurityExceptions(ctx, namespace)
if err != nil {
logger.L().Ctx(ctx).Warning("failed to get CRD security exceptions", helpers.Error(err))
} else if len(seList) > 0 || len(cseList) > 0 {
crdPolicies := convertToVulnerabilityExceptionPolicies(seList, cseList)
vulnExceptionList = append(vulnExceptionList, crdPolicies...)
}

return vulnExceptionList, nil
}

Expand Down
64 changes: 60 additions & 4 deletions adapters/v1/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ import (
beClientV1 "github.com/kubescape/backend/pkg/client/v1"
sysreport "github.com/kubescape/backend/pkg/server/v1/systemreports"
"github.com/kubescape/kubevuln/core/domain"
sev1beta1 "github.com/kubescape/kubevuln/pkg/securityexception/v1beta1"
"github.com/kubescape/kubevuln/repositories"
"github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestBackendAdapter_GetCVEExceptions(t *testing.T) {
Expand Down Expand Up @@ -71,8 +74,9 @@ func TestBackendAdapter_GetCVEExceptions(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &BackendAdapter{
clusterConfig: tt.fields.clusterConfig,
getCVEExceptionsFunc: tt.fields.getCVEExceptionsFunc,
clusterConfig: tt.fields.clusterConfig,
getCVEExceptionsFunc: tt.fields.getCVEExceptionsFunc,
securityExceptionRepo: &repositories.NoOpSecurityExceptionRepository{},
}
ctx := context.TODO()
if tt.workload {
Expand Down Expand Up @@ -190,7 +194,8 @@ func TestBackendAdapter_SubmitCVE(t *testing.T) {
getCVEExceptionsFunc: func(s, a string, designator *identifiers.PortalDesignator, headers map[string]string) ([]armotypes.VulnerabilityExceptionPolicy, error) {
return tt.exceptions, nil
},
httpPostFunc: httpPostFunc,
httpPostFunc: httpPostFunc,
securityExceptionRepo: &repositories.NoOpSecurityExceptionRepository{},
}
ctx := context.TODO()
ctx = context.WithValue(ctx, domain.TimestampKey{}, time.Now().Unix())
Expand Down Expand Up @@ -270,10 +275,11 @@ func TestNewBackendAdapter(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewBackendAdapter(tt.args.accountID, tt.args.apiServerRestURL, tt.args.eventReceiverRestURL, "")
got := NewBackendAdapter(tt.args.accountID, tt.args.apiServerRestURL, tt.args.eventReceiverRestURL, "", &repositories.NoOpSecurityExceptionRepository{})
// need to nil functions to compare
got.httpPostFunc = nil
got.getCVEExceptionsFunc = nil
got.securityExceptionRepo = nil
assert.NotEqual(t, got, tt.want)
})
}
Expand Down Expand Up @@ -494,3 +500,53 @@ func TestBackendAdapter_ReportScanFailure_NilError(t *testing.T) {
assert.Equal(t, scanfailure.ReasonSBOMIncomplete, capturedReport.FailureReason)
assert.Empty(t, capturedReport.Error, "Error field should be empty when scanErr is nil")
}

type mockSecurityExceptionRepo struct {
exceptions []sev1beta1.SecurityException
clusterExceptions []sev1beta1.ClusterSecurityException
err error
}

func (m *mockSecurityExceptionRepo) GetSecurityExceptions(_ context.Context, _ string) ([]sev1beta1.SecurityException, []sev1beta1.ClusterSecurityException, error) {
return m.exceptions, m.clusterExceptions, m.err
}

func TestGetCVEExceptions_MergesCRDExceptions(t *testing.T) {
cloudPolicies := []armotypes.VulnerabilityExceptionPolicy{
{
PolicyType: "vulnerabilityExceptionPolicy",
VulnerabilityPolicies: []armotypes.VulnerabilityPolicy{{Name: "CVE-CLOUD-1"}},
},
}

mockRepo := &mockSecurityExceptionRepo{
exceptions: []sev1beta1.SecurityException{
{
ObjectMeta: metav1.ObjectMeta{Namespace: "default"},
Spec: sev1beta1.SecurityExceptionSpec{
Vulnerabilities: []sev1beta1.VulnerabilityException{
{Vulnerability: sev1beta1.VulnerabilityRef{ID: "CVE-CRD-1"}},
},
},
},
},
}

a := &BackendAdapter{
clusterConfig: armometadata.ClusterConfig{AccountID: "test-account"},
getCVEExceptionsFunc: func(string, string, *identifiers.PortalDesignator, map[string]string) ([]armotypes.VulnerabilityExceptionPolicy, error) {
return cloudPolicies, nil
},
securityExceptionRepo: mockRepo,
}

ctx := context.WithValue(context.Background(), domain.WorkloadKey{}, domain.ScanCommand{
Wlid: "wlid://cluster-test/namespace-default/deployment-myapp",
})

exceptions, err := a.GetCVEExceptions(ctx)
require.NoError(t, err)
assert.Len(t, exceptions, 2, "should merge cloud + CRD exceptions")
assert.Equal(t, "CVE-CLOUD-1", exceptions[0].VulnerabilityPolicies[0].Name)
assert.Equal(t, "CVE-CRD-1", exceptions[1].VulnerabilityPolicies[0].Name)
}
112 changes: 112 additions & 0 deletions adapters/v1/securityexception.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package v1

import (
"time"

"github.com/armosec/armoapi-go/armotypes"
"github.com/armosec/armoapi-go/identifiers"
sev1beta1 "github.com/kubescape/kubevuln/pkg/securityexception/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// convertToVulnerabilityExceptionPolicies converts SecurityException and
// ClusterSecurityException CRDs into armotypes.VulnerabilityExceptionPolicy
// slices compatible with the existing exception pipeline.
func convertToVulnerabilityExceptionPolicies(exceptions []sev1beta1.SecurityException, clusterExceptions []sev1beta1.ClusterSecurityException) []armotypes.VulnerabilityExceptionPolicy {
var policies []armotypes.VulnerabilityExceptionPolicy

now := time.Now()

for i := range exceptions {
se := &exceptions[i]
if isExpired(se.Spec.ExpiresAt, now) {
continue
}
namespace := se.Namespace
for _, vuln := range se.Spec.Vulnerabilities {
p := buildPolicy(se.Spec, vuln, namespace)
policies = append(policies, p)
}
}

for i := range clusterExceptions {
cse := &clusterExceptions[i]
if isExpired(cse.Spec.ExpiresAt, now) {
continue
}
for _, vuln := range cse.Spec.Vulnerabilities {
p := buildPolicy(cse.Spec, vuln, "")
policies = append(policies, p)
}
}

return policies
}

func isExpired(expiresAt *metav1.Time, now time.Time) bool {
return expiresAt != nil && expiresAt.Time.Before(now)
}

func buildPolicy(spec sev1beta1.SecurityExceptionSpec, vuln sev1beta1.VulnerabilityException, namespace string) armotypes.VulnerabilityExceptionPolicy {
p := armotypes.VulnerabilityExceptionPolicy{
PolicyType: "vulnerabilityExceptionPolicy",
Actions: []armotypes.VulnerabilityExceptionPolicyActions{armotypes.Ignore},
VulnerabilityPolicies: []armotypes.VulnerabilityPolicy{
{Name: vuln.Vulnerability.ID},
},
Reason: spec.Reason,
}

if spec.ExpiresAt != nil {
t := spec.ExpiresAt.Time
p.ExpirationDate = &t
}

if vuln.ExpiredOnFix {
b := true
p.ExpiredOnFix = &b
}

p.Designatores = buildDesignators(spec.Match.Resources, namespace)

return p
}

func buildDesignators(resources []sev1beta1.ResourceMatch, namespace string) []identifiers.PortalDesignator {
if len(resources) == 0 {
// No specific resource match — create a namespace-only designator
if namespace != "" {
return []identifiers.PortalDesignator{
{
DesignatorType: identifiers.DesignatorAttributes,
Attributes: map[string]string{
"namespace": namespace,
},
},
}
}
return nil
}

designators := make([]identifiers.PortalDesignator, 0, len(resources))
for _, r := range resources {
attrs := map[string]string{}
if namespace != "" {
attrs["namespace"] = namespace
}
if r.Kind != "" {
attrs["kind"] = r.Kind
}
if r.Name != "" {
attrs["name"] = r.Name
}
if len(attrs) == 0 {
continue
}
designators = append(designators, identifiers.PortalDesignator{
DesignatorType: identifiers.DesignatorAttributes,
Attributes: attrs,
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return designators
}
Loading
Loading