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
22 changes: 19 additions & 3 deletions pkg/registry/file/configurationscansummarystorage.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,25 @@ func (s *ConfigurationScanSummaryStorage) GetList(ctx context.Context, key strin

workloadScanSummaryListObjPtr := &softwarecomposition.WorkloadConfigurationScanSummaryList{}

// ask for all workloadconfigurationscansummaries in the cluster
if err := s.realStore.GetList(ctx, "/spdx.softwarecomposition.kubescape.io/"+workloadConfigurationScanSummariesResource, opts, workloadScanSummaryListObjPtr); err != nil {
return err
// ask for all workloadconfigurationscansummaries in the cluster.
// The underlying store caps each page at a default limit and returns a
// continuation token, so we must page through every token to make sure the
// aggregation covers all objects and not just the first page (issue #337).
listOpts := opts
listOpts.Predicate.Continue = ""
for {
page := &softwarecomposition.WorkloadConfigurationScanSummaryList{}
if err := s.realStore.GetList(ctx, "/spdx.softwarecomposition.kubescape.io/"+workloadConfigurationScanSummariesResource, listOpts, page); err != nil {
return err
}
workloadScanSummaryListObjPtr.Items = append(workloadScanSummaryListObjPtr.Items, page.Items...)

// the store only sets a continuation token when the page was truncated;
// an empty token means we have read the last page.
listOpts.Predicate.Continue = page.Continue
if listOpts.Predicate.Continue == "" {
break
}
}

// generate a single configurationScanSummary for the cluster, with a configuration scan summary for each namespace
Expand Down
71 changes: 71 additions & 0 deletions pkg/registry/file/configurationscansummarystorage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package file

import (
"context"
"fmt"
"testing"
"time"

Expand Down Expand Up @@ -201,6 +202,76 @@ func TestConfigurationScanSummaryStorage_GetList(t *testing.T) {
}
}

func TestConfigurationScanSummaryStorage_GetList_Pagination(t *testing.T) {
// Regression test for issue #337: the underlying store caps each page at a
// default limit of 500 objects and returns a continuation token. GetList must
// consume that token so the cluster-wide aggregation covers every object,
// otherwise namespaces beyond the first page silently disappear and per-namespace
// severity counters are wrong.
const (
alphaCount = 500 // fills the entire first page on its own
betaCount = 200 // lives entirely beyond the first page
)

pool := NewTestPool(t.TempDir())
require.NotNil(t, pool)
defer func(pool *sqlitemigration.Pool) {
_ = pool.Close()
}(pool)
sch := scheme.Scheme
require.NoError(t, softwarecomposition.AddToScheme(sch))
realStorage := NewStorageImpl(afero.NewMemMapFs(), "/", pool, nil, sch)

ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
defer cancel()

// objects are listed ordered by creation (rowid), so creating all of "alpha"
// before "beta" guarantees "beta" falls entirely on the second page.
createWorkloadSummaries := func(namespace string, count int) {
for i := range count {
name := fmt.Sprintf("workload-%d", i)
wlObj := &softwarecomposition.WorkloadConfigurationScanSummary{
ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: softwarecomposition.WorkloadConfigurationScanSummarySpec{
Severities: softwarecomposition.WorkloadConfigurationScanSeveritiesSummary{
High: 1,
},
},
}
key := fmt.Sprintf("/spdx.softwarecomposition.kubescape.io/workloadconfigurationscansummaries/%s/%s", namespace, name)
require.NoError(t, realStorage.Create(ctx, key, wlObj, nil, 0))
}
}
createWorkloadSummaries("alpha", alphaCount)
createWorkloadSummaries("beta", betaCount)

configScanSummaryStorage := NewConfigurationScanSummaryStorage(realStorage)

listObj := &v1beta1.ConfigurationScanSummaryList{}
// the aggregation reads severities from the spec, so request the full spec
err := configScanSummaryStorage.GetList(ctx, "/spdx.softwarecomposition.kubescape.io/configurationscansummaries", storage.ListOptions{
ResourceVersion: softwarecomposition.ResourceVersionFullSpec,
Predicate: storage.SelectionPredicate{},
}, listObj)
require.NoError(t, err)

// Both namespaces must be present even though "beta" lives entirely on the
// second page. Before the fix, only the first page (500 objects) was read, so
// "beta" disappeared completely and the list contained a single namespace.
require.Len(t, listObj.Items, 2, "both namespaces must be present, including the one beyond the first page")

// The aggregated severities must cover every object, not just the first page.
// Before the fix this totalled only 500 (the truncated first page).
var totalHigh int64
for _, item := range listObj.Items {
totalHigh += item.Spec.Severities.High
}
assert.Equal(t, int64(alphaCount+betaCount), totalHigh, "severities must aggregate all objects across every page")
}

func TestGenerateConfigurationScanSummary(t *testing.T) {
tests := []struct {
name string
Expand Down
22 changes: 19 additions & 3 deletions pkg/registry/file/vulnerabilitysummarystorage.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,25 @@ func (s *VulnerabilitySummaryStorage) GetList(ctx context.Context, key string, o

vulnerabilityManifestSummaryListObjPtr := &softwarecomposition.VulnerabilityManifestSummaryList{}

// ask for all vulnerabilitySummaries in the cluster
if err := s.realStore.GetList(ctx, "/spdx.softwarecomposition.kubescape.io/"+vulnerabilitySummariesResource, opts, vulnerabilityManifestSummaryListObjPtr); err != nil {
return err
// ask for all vulnerabilitySummaries in the cluster.
// The underlying store caps each page at a default limit and returns a
// continuation token, so we must page through every token to make sure the
// aggregation covers all objects and not just the first page (issue #337).
listOpts := opts
listOpts.Predicate.Continue = ""
for {
page := &softwarecomposition.VulnerabilityManifestSummaryList{}
if err := s.realStore.GetList(ctx, "/spdx.softwarecomposition.kubescape.io/"+vulnerabilitySummariesResource, listOpts, page); err != nil {
return err
}
vulnerabilityManifestSummaryListObjPtr.Items = append(vulnerabilityManifestSummaryListObjPtr.Items, page.Items...)

// the store only sets a continuation token when the page was truncated;
// an empty token means we have read the last page.
listOpts.Predicate.Continue = page.Continue
if listOpts.Predicate.Continue == "" {
break
}
}

// generate a single vulnerabilitySummary for the cluster, with a vulnerability summary for each namespace
Expand Down
61 changes: 61 additions & 0 deletions pkg/registry/file/vulnerabilitysummarystorage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package file

import (
"context"
"fmt"
"testing"
"time"

Expand Down Expand Up @@ -268,6 +269,66 @@ func TestVulnSummaryStorageImpl_GetList(t *testing.T) {
}
}

func TestVulnSummaryStorageImpl_GetList_Pagination(t *testing.T) {
// Regression test for issue #337: the underlying store caps each page at a
// default limit of 500 objects and returns a continuation token. GetList must
// consume that token so the cluster-wide aggregation covers every object,
// otherwise namespaces beyond the first page silently disappear.
const (
alphaCount = 500 // fills the entire first page on its own
betaCount = 200 // lives entirely beyond the first page
)

pool := NewTestPool(t.TempDir())
require.NotNil(t, pool)
defer func(pool *sqlitemigration.Pool) {
_ = pool.Close()
}(pool)
sch := scheme.Scheme
require.NoError(t, softwarecomposition.AddToScheme(sch))
realStorage := NewStorageImpl(afero.NewMemMapFs(), "/", pool, nil, sch)

ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
defer cancel()

// objects are listed ordered by creation (rowid), so creating all of "alpha"
// before "beta" guarantees "beta" falls entirely on the second page.
createManifestSummaries := func(namespace string, count int) {
for i := range count {
name := fmt.Sprintf("workload-%d", i)
obj := &softwarecomposition.VulnerabilityManifestSummary{
ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: namespace,
},
}
key := fmt.Sprintf("/spdx.softwarecomposition.kubescape.io/vulnerabilitymanifestsummaries/%s/%s", namespace, name)
require.NoError(t, realStorage.Create(ctx, key, obj, nil, 0))
}
}
createManifestSummaries("alpha", alphaCount)
createManifestSummaries("beta", betaCount)

s := NewVulnerabilitySummaryStorage(realStorage)
o := &softwarecomposition.VulnerabilitySummaryList{}
require.NoError(t, s.GetList(ctx, "/spdx.softwarecomposition.kubescape.io/vulnerabilitysummaries", storage.ListOptions{}, o))

// Both namespaces must be present even though "beta" lives entirely on the
// second page. Before the fix, only the first page (500 objects) was read, so
// "beta" disappeared and the list contained a single namespace.
require.Len(t, o.Items, 2, "both namespaces must be present, including the one beyond the first page")

// Each manifest summary contributes one scope to its namespace's summary, so
// the total scope count must equal every created object. Before the fix this
// totalled only 500 (the truncated first page).
scopesByNamespace := map[string]int{}
for _, item := range o.Items {
scopesByNamespace[item.Name] = len(item.Spec.WorkloadVulnerabilitiesObj)
}
assert.Equal(t, alphaCount, scopesByNamespace["alpha"], "alpha must aggregate all objects")
assert.Equal(t, betaCount, scopesByNamespace["beta"], "beta must not be truncated/dropped by the page limit")
}

func TestVulnSummaryStorageImpl_GuaranteedUpdate(t *testing.T) {
type args struct {
key string
Expand Down
Loading