Skip to content
Open
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
20 changes: 20 additions & 0 deletions central/reports/common/query_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,17 @@ func (q *queryBuilder) buildEntityScopeQuery() (*v1.Query, error) {
for _, rv := range rule.GetValues() {
val := rv.GetValue()
key, value := splitLabelValue(val)
if rv.GetMatchType() == storage.MatchType_REGEX {
// For regex match, only apply the r/ prefix to parts that contain
// regex metacharacters. Literal parts stay quoted for exact matching.
rawParts := strings.SplitN(val, "=", 2)
rawValue := ""
if len(rawParts) == 2 {
rawValue = rawParts[1]
}
key = mapPartQueryString(rawParts[0])
value = mapPartQueryString(rawValue)
}
mapQueries = append(mapQueries,
search.NewQueryBuilder().AddMapQuery(fieldLabel, key, value).ProtoQuery())
}
Expand Down Expand Up @@ -253,3 +264,12 @@ func splitLabelValue(labelVal string) (string, string) {
}
return fmt.Sprintf("%q", labelVal), fmt.Sprintf("%q", "")
}

// mapPartQueryString returns a regex query string (r/...) if the part dos not contain
// a regex prefix; otherwise returns the part.
func mapPartQueryString(part string) string {
if strings.HasPrefix(part, search.RegexPrefix) {
return part
}
return search.RegexQueryString(part)
}
34 changes: 34 additions & 0 deletions central/reports/common/query_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,40 @@ func TestBuildEntityScopeQuery(t *testing.T) {
expected: search.NewQueryBuilder().AddStrings(search.DeploymentName, "r/web-.*").ProtoQuery(),
assertQueries: assertByDirectComparison,
},
{
name: "Namespace label regex match type with regex prefix",
scope: &storage.EntityScope{
Rules: []*storage.EntityScopeRule{
{
Entity: storage.EntityType_ENTITY_TYPE_NAMESPACE,
Field: storage.EntityField_FIELD_LABEL,
Values: []*storage.RuleValue{
{Value: "r/env=pr.*", MatchType: storage.MatchType_REGEX},
},
},
},
},
// key "env" has regex prefix
expected: search.NewQueryBuilder().AddMapQuery(search.NamespaceLabel, "r/env", "r/pr.*").ProtoQuery(),
assertQueries: assertByDirectComparison,
},
{
name: "Namespace label regex match type",
scope: &storage.EntityScope{
Rules: []*storage.EntityScopeRule{
{
Entity: storage.EntityType_ENTITY_TYPE_NAMESPACE,
Field: storage.EntityField_FIELD_LABEL,
Values: []*storage.RuleValue{
{Value: "env=pr.*", MatchType: storage.MatchType_REGEX},
},
},
},
},
// key "env" has no metacharacters → exact match; value "pr.*" has "." and "*" → regex
expected: search.NewQueryBuilder().AddMapQuery(search.NamespaceLabel, "r/env", "r/pr.*").ProtoQuery(),
assertQueries: assertByDirectComparison,
},
{
name: "Rule with empty values is skipped",
scope: &storage.EntityScope{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
clusterDSMocks "github.com/stackrox/rox/central/cluster/datastore/mocks"
"github.com/stackrox/rox/central/graphql/resolvers"
"github.com/stackrox/rox/central/graphql/resolvers/loaders"
namespaceDSMocks "github.com/stackrox/rox/central/namespace/datastore/mocks"
namespaceDS "github.com/stackrox/rox/central/namespace/datastore"
collectionDS "github.com/stackrox/rox/central/resourcecollection/datastore"
collectionPostgres "github.com/stackrox/rox/central/resourcecollection/datastore/store/postgres"
deploymentsView "github.com/stackrox/rox/central/views/deployments"
Expand Down Expand Up @@ -50,7 +50,7 @@ type NewDataModelEnhancedReportingTestSuite struct {
testDB *pgtest.TestPostgres
watchedImageDatastore watchedImageDS.DataStore
clusterDatastore *clusterDSMocks.MockDataStore
namespaceDatastore *namespaceDSMocks.MockDataStore
namespaceDatastore namespaceDS.DataStore
reportGenerator *reportGeneratorImpl
}

Expand All @@ -60,6 +60,7 @@ func (s *NewDataModelEnhancedReportingTestSuite) TearDownSuite() {
s.truncateTable(postgresSchema.ImageComponentV2TableName)
s.truncateTable(postgresSchema.ImageCvesV2TableName)
s.truncateTable(postgresSchema.CollectionsTableName)
s.truncateTable(postgresSchema.NamespacesTableName)
}

func (s *NewDataModelEnhancedReportingTestSuite) SetupSuite() {
Expand Down Expand Up @@ -98,7 +99,9 @@ func (s *NewDataModelEnhancedReportingTestSuite) SetupSuite() {
_, collectionQueryResolver, err := collectionDS.New(collectionStore)
s.NoError(err)
s.clusterDatastore = clusterDSMocks.NewMockDataStore(mockCtrl)
s.namespaceDatastore = namespaceDSMocks.NewMockDataStore(mockCtrl)
var nsErr error
s.namespaceDatastore, nsErr = namespaceDS.GetTestPostgresDataStore(s.T(), s.testDB.DB)
s.Require().NoError(nsErr)

// Add Test Data to DataStores
clusters := []*storage.Cluster{
Expand All @@ -107,6 +110,18 @@ func (s *NewDataModelEnhancedReportingTestSuite) SetupSuite() {
}

namespaces := testNamespaces(clusters, 2)
// Add labels to namespaces so entity scope label queries can be tested:
// ns1 gets env=prod, ns2 gets env=dev.
for _, ns := range namespaces {
if ns.GetName() == "ns1" {
ns.Labels = map[string]string{"env": "prod"}
} else {
ns.Labels = map[string]string{"env": "dev"}
}
nsErr := s.namespaceDatastore.AddNamespace(s.ctx, ns)
s.Require().NoError(nsErr)
}

deployments, images := testDeploymentsWithImages(namespaces, 1)
// insert deployments in deployment table
for _, dep := range deployments {
Expand Down Expand Up @@ -146,9 +161,6 @@ func (s *NewDataModelEnhancedReportingTestSuite) SetupSuite() {
s.clusterDatastore.EXPECT().GetClusters(gomock.Any()).
Return(clusters, nil).AnyTimes()

s.namespaceDatastore.EXPECT().GetAllNamespaces(gomock.Any()).
Return(namespaces, nil).AnyTimes()

blobStore := blobDS.NewTestDatastore(s.T(), s.testDB.DB)

s.reportGenerator = newReportGeneratorImpl(s.testDB, nil, resolver.DeploymentDataStore,
Expand Down Expand Up @@ -530,6 +542,38 @@ func (s *NewDataModelEnhancedReportingTestSuite) TestGetReportDataWithEntityScop
},
},
},
"Namespace label regex scope with CVSS filter": {
// ns1 has label env=prod; regex "env=pr.*" matches prod but not dev.
// With all-cluster SAC access and CVSS:>=7.0, only fixable_critical CVEs are returned
// for deployments in ns1 across both clusters, plus watched images.
entityScope: &storage.EntityScope{
Rules: []*storage.EntityScopeRule{
{
Entity: storage.EntityType_ENTITY_TYPE_NAMESPACE,
Field: storage.EntityField_FIELD_LABEL,
Values: []*storage.RuleValue{
{Value: "env=pr.*", MatchType: storage.MatchType_REGEX},
},
},
},
},
query: "CVSS:>=7.0",
imageTypes: allImageTypes,
scopeRules: []*storage.SimpleAccessScope_Rules{
{IncludedClusters: []string{"c1", "c2"}},
},
expected: &vulnReportDataNewDataModel{
deploymentNames: []string{"c1_ns1_dep0", "c2_ns1_dep0"},
imageNames: []string{"c1_ns1_dep0_img", "c2_ns1_dep0_img", "w0_img", "w1_img"},
componentNames: []string{"c1_ns1_dep0_img_comp", "c2_ns1_dep0_img_comp", "w0_img_comp", "w1_img_comp"},
cveNames: []string{
"CVE-fixable_critical-c1_ns1_dep0_img_comp",
"CVE-fixable_critical-c2_ns1_dep0_img_comp",
"CVE-fixable_critical-w0_img_comp",
"CVE-fixable_critical-w1_img_comp",
},
},
},
"Cluster scope deployed-only with image regex and EPSS combined filter": {
entityScope: &storage.EntityScope{
Rules: []*storage.EntityScopeRule{
Expand Down
6 changes: 4 additions & 2 deletions central/reports/validation/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,10 @@ func validateEntityScope(es *apiV2.EntityScope) error {
return errors.Wrapf(errox.InvalidArgs, "%v values must be in 'key=value' format", rule.GetField())
}
// Check the key for a Kubernetes qualified name.
if errs := k8sValidation.IsLabelKey(mapKey); len(errs) > 0 {
return errors.Wrapf(errox.InvalidArgs, "invalid %v key %q: %s", rule.GetField(), mapKey, strings.Join(errs, "; "))
if rv.GetMatchType() == apiV2.MatchType_EXACT {
if errs := k8sValidation.IsLabelKey(mapKey); len(errs) > 0 {
return errors.Wrapf(errox.InvalidArgs, "invalid %v key %q: %s", rule.GetField(), mapKey, strings.Join(errs, "; "))
}
}
valOfValue = mapValue
}
Expand Down
Loading