Skip to content

Commit e89acd6

Browse files
committed
ROX-33655: Query changes for enhanced filters
1 parent 8c9e377 commit e89acd6

File tree

7 files changed

+678
-16
lines changed

7 files changed

+678
-16
lines changed

central/reports/common/query_builder.go

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77
"time"
88

9+
"github.com/pkg/errors"
910
collectionDataStore "github.com/stackrox/rox/central/resourcecollection/datastore"
1011
v1 "github.com/stackrox/rox/generated/api/v1"
1112
"github.com/stackrox/rox/generated/storage"
@@ -27,17 +28,20 @@ type queryBuilder struct {
2728
collection *storage.ResourceCollection
2829
collectionQueryResolver collectionDataStore.QueryResolver
2930
dataStartTime time.Time
31+
entityScope *storage.EntityScope
3032
}
3133

3234
// NewVulnReportQueryBuilder builds a query builder to build scope and cve filtering queries for vuln reporting
33-
func NewVulnReportQueryBuilder(collection *storage.ResourceCollection, vulnFilters *storage.VulnerabilityReportFilters,
35+
func NewVulnReportQueryBuilder(collection *storage.ResourceCollection, entityScope *storage.EntityScope, vulnFilters *storage.VulnerabilityReportFilters,
3436
collectionQueryRes collectionDataStore.QueryResolver, dataStartTime time.Time) *queryBuilder {
3537
return &queryBuilder{
36-
vulnFilters: vulnFilters,
3738
collection: collection,
39+
entityScope: entityScope,
40+
vulnFilters: vulnFilters,
3841
collectionQueryResolver: collectionQueryRes,
3942
dataStartTime: dataStartTime,
4043
}
44+
4145
}
4246

4347
// BuildQuery builds scope and cve filtering queries for vuln reporting
@@ -46,7 +50,13 @@ func (q *queryBuilder) BuildQuery(
4650
clusters []effectiveaccessscope.Cluster,
4751
namespaces []effectiveaccessscope.Namespace,
4852
) (*ReportQuery, error) {
49-
deploymentsQuery, err := q.collectionQueryResolver.ResolveCollectionQuery(ctx, q.collection)
53+
deploymentsQuery := search.EmptyQuery()
54+
var err error
55+
if q.collection != nil {
56+
deploymentsQuery, err = q.collectionQueryResolver.ResolveCollectionQuery(ctx, q.collection)
57+
} else if q.entityScope != nil {
58+
deploymentsQuery, err = q.buildEntityScopeQuery()
59+
}
5060
if err != nil {
5161
return nil, err
5262
}
@@ -66,7 +76,9 @@ func (q *queryBuilder) BuildQuery(
6676
}, nil
6777
}
6878

69-
func (q *queryBuilder) buildCVEAttributesQuery() (string, error) {
79+
// addSeverityFixabilityFiltersCollectionScopedReports adds severity, fixability filters for collection scoped reports
80+
func (q *queryBuilder) addSeverityFixabilityFiltersCollectionScopedReports() []string {
81+
7082
vulnReportFilters := q.vulnFilters
7183
var conjuncts []string
7284

@@ -86,13 +98,26 @@ func (q *queryBuilder) buildCVEAttributesQuery() (string, error) {
8698
if len(severities) > 0 {
8799
conjuncts = append(conjuncts, search.NewQueryBuilder().AddExactMatches(search.Severity, severities...).Query())
88100
}
101+
return conjuncts
102+
}
103+
104+
func (q *queryBuilder) buildCVEAttributesQuery() (string, error) {
89105

106+
vulnReportFilters := q.vulnFilters
107+
var conjuncts []string
108+
109+
if q.collection != nil {
110+
// for collections only add fixability, severity filters for CVE
111+
conjuncts = q.addSeverityFixabilityFiltersCollectionScopedReports()
112+
} else if q.entityScope != nil {
113+
// for entity scoped reports add all the search filters from query string
114+
conjuncts = append(conjuncts, q.vulnFilters.GetQuery())
115+
}
90116
if filterVulnsByFirstOccurrenceTime(vulnReportFilters) {
91117
startTimeStr := fmt.Sprintf(">=%s", q.dataStartTime.Format("01/02/2006 3:04:05 PM MST"))
92118
tsQ := search.NewQueryBuilder().AddStrings(search.FirstImageOccurrenceTimestamp, startTimeStr).Query()
93119
conjuncts = append(conjuncts, tsQ)
94120
}
95-
96121
return strings.Join(conjuncts, "+"), nil
97122
}
98123

@@ -129,6 +154,100 @@ func (q *queryBuilder) buildAccessScopeQuery(
129154
return scopeQuery, nil
130155
}
131156

157+
// buildEntityScopeQuery uses entity scope object to build v1 query
158+
func (q *queryBuilder) buildEntityScopeQuery() (*v1.Query, error) {
159+
rules := q.entityScope.GetRules()
160+
if len(rules) == 0 {
161+
return search.EmptyQuery(), nil
162+
}
163+
164+
var conjuncts []*v1.Query
165+
for _, rule := range rules {
166+
fieldLabel, err := entityScopeRuleToFieldLabel(rule)
167+
if err != nil {
168+
return nil, err
169+
}
170+
isMapField := fieldLabel == search.DeploymentLabel ||
171+
fieldLabel == search.NamespaceLabel ||
172+
fieldLabel == search.ClusterLabel ||
173+
fieldLabel == search.DeploymentAnnotation ||
174+
fieldLabel == search.NamespaceAnnotation
175+
176+
values := make([]string, 0, len(rule.GetValues()))
177+
for _, rv := range rule.GetValues() {
178+
val := rv.GetValue()
179+
if rv.GetMatchType() == storage.MatchType_REGEX {
180+
val = search.RegexPrefix + val
181+
}
182+
values = append(values, val)
183+
}
184+
185+
if len(values) == 0 {
186+
continue
187+
}
188+
189+
if isMapField {
190+
for _, v := range values {
191+
key, value := splitLabelValue(v)
192+
conjuncts = append(conjuncts,
193+
search.NewQueryBuilder().AddMapQuery(fieldLabel, key, value).ProtoQuery())
194+
}
195+
} else if rule.GetValues()[0].GetMatchType() == storage.MatchType_REGEX {
196+
conjuncts = append(conjuncts,
197+
search.NewQueryBuilder().AddStrings(fieldLabel, values...).ProtoQuery())
198+
} else {
199+
conjuncts = append(conjuncts,
200+
search.NewQueryBuilder().AddExactMatches(fieldLabel, values...).ProtoQuery())
201+
}
202+
}
203+
204+
if len(conjuncts) == 0 {
205+
return search.EmptyQuery(), nil
206+
}
207+
return search.ConjunctionQuery(conjuncts...), nil
208+
}
209+
210+
// entityScopeRuleToFieldLabel returns search filter for given entity field pair
211+
func entityScopeRuleToFieldLabel(rule *storage.EntityScopeRule) (search.FieldLabel, error) {
212+
switch rule.GetEntity() {
213+
case storage.EntityType_ENTITY_TYPE_DEPLOYMENT:
214+
switch rule.GetField() {
215+
case storage.EntityField_FIELD_NAME:
216+
return search.DeploymentName, nil
217+
case storage.EntityField_FIELD_LABEL:
218+
return search.DeploymentLabel, nil
219+
case storage.EntityField_FIELD_ANNOTATION:
220+
return search.DeploymentAnnotation, nil
221+
}
222+
case storage.EntityType_ENTITY_TYPE_NAMESPACE:
223+
switch rule.GetField() {
224+
case storage.EntityField_FIELD_NAME:
225+
return search.Namespace, nil
226+
case storage.EntityField_FIELD_LABEL:
227+
return search.NamespaceLabel, nil
228+
case storage.EntityField_FIELD_ANNOTATION:
229+
return search.NamespaceAnnotation, nil
230+
}
231+
case storage.EntityType_ENTITY_TYPE_CLUSTER:
232+
switch rule.GetField() {
233+
case storage.EntityField_FIELD_NAME:
234+
return search.Cluster, nil
235+
case storage.EntityField_FIELD_LABEL:
236+
return search.ClusterLabel, nil
237+
}
238+
}
239+
return "", errors.Errorf("Unsupported entity/field combination %s/%s", rule.GetEntity(), rule.GetField())
240+
}
241+
132242
func filterVulnsByFirstOccurrenceTime(vulnReportFilters *storage.VulnerabilityReportFilters) bool {
133243
return vulnReportFilters.GetSinceLastSentScheduledReport() || vulnReportFilters.GetSinceStartDate() != nil
134244
}
245+
246+
// split map field values like namespace labels(key=val) to key,val pair
247+
func splitLabelValue(labelVal string) (string, string) {
248+
parts := strings.SplitN(labelVal, "=", 2)
249+
if len(parts) == 2 {
250+
return fmt.Sprintf("%q", parts[0]), fmt.Sprintf("%q", parts[1])
251+
}
252+
return fmt.Sprintf("%q", labelVal), fmt.Sprintf("%q", "")
253+
}

central/reports/common/query_builder_test.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,189 @@ func TestBuildAccessScopeQuery(t *testing.T) {
197197
func assertByDirectComparison(t testing.TB, expected *v1.Query, actual *v1.Query) {
198198
protoassert.Equal(t, expected, actual)
199199
}
200+
201+
func TestBuildEntityScopeQuery(t *testing.T) {
202+
testCases := []struct {
203+
name string
204+
scope *storage.EntityScope
205+
expected *v1.Query
206+
assertQueries func(t testing.TB, expected *v1.Query, actual *v1.Query)
207+
hasError bool
208+
}{
209+
{
210+
name: "Empty rules returns empty query (match all)",
211+
scope: &storage.EntityScope{},
212+
expected: search.EmptyQuery(),
213+
assertQueries: assertByDirectComparison,
214+
},
215+
{
216+
name: "Namespace rule",
217+
scope: &storage.EntityScope{
218+
Rules: []*storage.EntityScopeRule{
219+
{
220+
Entity: storage.EntityType_ENTITY_TYPE_NAMESPACE,
221+
Field: storage.EntityField_FIELD_NAME,
222+
Values: []*storage.RuleValue{
223+
{Value: "prod", MatchType: storage.MatchType_EXACT},
224+
{Value: "staging", MatchType: storage.MatchType_EXACT},
225+
},
226+
},
227+
},
228+
},
229+
expected: search.NewQueryBuilder().AddExactMatches(search.Namespace, "prod", "staging").ProtoQuery(),
230+
assertQueries: assertByDirectComparison,
231+
},
232+
{
233+
name: "Single deployment name rule",
234+
scope: &storage.EntityScope{
235+
Rules: []*storage.EntityScopeRule{
236+
{
237+
Entity: storage.EntityType_ENTITY_TYPE_DEPLOYMENT,
238+
Field: storage.EntityField_FIELD_NAME,
239+
Values: []*storage.RuleValue{
240+
{Value: "web-server", MatchType: storage.MatchType_EXACT},
241+
},
242+
},
243+
},
244+
},
245+
expected: search.NewQueryBuilder().AddExactMatches(search.DeploymentName, "web-server").ProtoQuery(),
246+
assertQueries: assertByDirectComparison,
247+
},
248+
{
249+
name: "Cluster name rule",
250+
scope: &storage.EntityScope{
251+
Rules: []*storage.EntityScopeRule{
252+
{
253+
Entity: storage.EntityType_ENTITY_TYPE_CLUSTER,
254+
Field: storage.EntityField_FIELD_NAME,
255+
Values: []*storage.RuleValue{
256+
{Value: "prod-us", MatchType: storage.MatchType_EXACT},
257+
{Value: "prod-eu", MatchType: storage.MatchType_EXACT},
258+
},
259+
},
260+
},
261+
},
262+
expected: search.NewQueryBuilder().AddExactMatches(search.Cluster, "prod-us", "prod-eu").ProtoQuery(),
263+
assertQueries: assertByDirectComparison,
264+
},
265+
{
266+
name: "Multiple rules are ANDed",
267+
scope: &storage.EntityScope{
268+
Rules: []*storage.EntityScopeRule{
269+
{
270+
Entity: storage.EntityType_ENTITY_TYPE_NAMESPACE,
271+
Field: storage.EntityField_FIELD_NAME,
272+
Values: []*storage.RuleValue{
273+
{Value: "prod", MatchType: storage.MatchType_EXACT},
274+
},
275+
},
276+
{
277+
Entity: storage.EntityType_ENTITY_TYPE_DEPLOYMENT,
278+
Field: storage.EntityField_FIELD_NAME,
279+
Values: []*storage.RuleValue{
280+
{Value: "backend", MatchType: storage.MatchType_EXACT},
281+
{Value: "frontend", MatchType: storage.MatchType_EXACT},
282+
},
283+
},
284+
},
285+
},
286+
expected: search.ConjunctionQuery(
287+
search.NewQueryBuilder().AddExactMatches(search.Namespace, "prod").ProtoQuery(),
288+
search.NewQueryBuilder().AddExactMatches(search.DeploymentName, "backend", "frontend").ProtoQuery(),
289+
),
290+
assertQueries: assertByDirectComparison,
291+
},
292+
{
293+
name: "Label rule uses map query",
294+
scope: &storage.EntityScope{
295+
Rules: []*storage.EntityScopeRule{
296+
{
297+
Entity: storage.EntityType_ENTITY_TYPE_NAMESPACE,
298+
Field: storage.EntityField_FIELD_LABEL,
299+
Values: []*storage.RuleValue{
300+
{Value: "env=prod", MatchType: storage.MatchType_EXACT},
301+
},
302+
},
303+
},
304+
},
305+
expected: search.NewQueryBuilder().AddMapQuery(search.NamespaceLabel, `"env"`, `"prod"`).ProtoQuery(),
306+
assertQueries: assertByDirectComparison,
307+
},
308+
{
309+
name: "Regex match type adds r/ prefix",
310+
scope: &storage.EntityScope{
311+
Rules: []*storage.EntityScopeRule{
312+
{
313+
Entity: storage.EntityType_ENTITY_TYPE_DEPLOYMENT,
314+
Field: storage.EntityField_FIELD_NAME,
315+
Values: []*storage.RuleValue{
316+
{Value: "web-.*", MatchType: storage.MatchType_REGEX},
317+
},
318+
},
319+
},
320+
},
321+
expected: search.NewQueryBuilder().AddStrings(search.DeploymentName, "r/web-.*").ProtoQuery(),
322+
assertQueries: assertByDirectComparison,
323+
},
324+
{
325+
name: "Rule with empty values is skipped",
326+
scope: &storage.EntityScope{
327+
Rules: []*storage.EntityScopeRule{
328+
{
329+
Entity: storage.EntityType_ENTITY_TYPE_NAMESPACE,
330+
Field: storage.EntityField_FIELD_NAME,
331+
Values: []*storage.RuleValue{},
332+
},
333+
},
334+
},
335+
expected: search.EmptyQuery(),
336+
assertQueries: assertByDirectComparison,
337+
},
338+
{
339+
name: "Unsupported entity/field returns error",
340+
scope: &storage.EntityScope{
341+
Rules: []*storage.EntityScopeRule{
342+
{
343+
Entity: storage.EntityType_ENTITY_TYPE_CLUSTER,
344+
Field: storage.EntityField_FIELD_ANNOTATION,
345+
Values: []*storage.RuleValue{
346+
{Value: "team=infra", MatchType: storage.MatchType_EXACT},
347+
},
348+
},
349+
},
350+
},
351+
hasError: true,
352+
},
353+
{
354+
name: "Deployment annotation rule",
355+
scope: &storage.EntityScope{
356+
Rules: []*storage.EntityScopeRule{
357+
{
358+
Entity: storage.EntityType_ENTITY_TYPE_DEPLOYMENT,
359+
Field: storage.EntityField_FIELD_ANNOTATION,
360+
Values: []*storage.RuleValue{
361+
{Value: "owner=team-a", MatchType: storage.MatchType_EXACT},
362+
},
363+
},
364+
},
365+
},
366+
expected: search.NewQueryBuilder().AddMapQuery(search.DeploymentAnnotation, `"owner"`, `"team-a"`).ProtoQuery(),
367+
assertQueries: assertByDirectComparison,
368+
},
369+
}
370+
371+
for _, tc := range testCases {
372+
t.Run(tc.name, func(t *testing.T) {
373+
qb := &queryBuilder{
374+
entityScope: tc.scope,
375+
}
376+
result, err := qb.buildEntityScopeQuery()
377+
if tc.hasError {
378+
assert.Error(t, err)
379+
return
380+
}
381+
assert.NoError(t, err)
382+
tc.assertQueries(t, tc.expected, result)
383+
})
384+
}
385+
}

central/reports/scheduler/schedule.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ func (s *scheduler) getReportData(ctx context.Context, rc *storage.ReportConfigu
361361

362362
func (s *scheduler) buildReportQuery(ctx context.Context, rc *storage.ReportConfiguration,
363363
collection *storage.ResourceCollection) (*common.ReportQuery, error) {
364-
qb := common.NewVulnReportQueryBuilder(collection, rc.GetVulnReportFilters(), s.collectionQueryResolver,
364+
qb := common.NewVulnReportQueryBuilder(collection, rc.GetResourceScope().GetEntityScope(), rc.GetVulnReportFilters(), s.collectionQueryResolver,
365365
timestamp.FromProtobuf(rc.GetLastSuccessfulRunTime()).GoTime())
366366
rQuery, err := qb.BuildQuery(ctx, nil, nil)
367367
if err != nil {

central/reports/scheduler/v2/reportgenerator/report_gen_impl.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ func (rg *reportGeneratorImpl) buildReportQueryViewBased(snap *storage.ReportSna
389389

390390
func (rg *reportGeneratorImpl) buildReportQuery(snap *storage.ReportSnapshot,
391391
collection *storage.ResourceCollection, dataStartTime time.Time) (*common.ReportQuery, error) {
392-
qb := common.NewVulnReportQueryBuilder(collection, snap.GetVulnReportFilters(), rg.collectionQueryResolver,
392+
qb := common.NewVulnReportQueryBuilder(collection, snap.GetResourceScope().GetEntityScope(), snap.GetVulnReportFilters(), rg.collectionQueryResolver,
393393
dataStartTime)
394394
allClusters, allNamespaces, err := rg.getClustersAndNamespacesForSAC()
395395
if err != nil {

0 commit comments

Comments
 (0)