Skip to content

Commit dd7d511

Browse files
committed
ROX-33655: Query changes for enahnced filters
1 parent 9c983b6 commit dd7d511

File tree

6 files changed

+774
-15
lines changed

6 files changed

+774
-15
lines changed

central/reports/common/query_builder.go

Lines changed: 122 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,20 @@ type queryBuilder struct {
2727
collection *storage.ResourceCollection
2828
collectionQueryResolver collectionDataStore.QueryResolver
2929
dataStartTime time.Time
30+
entityScope *storage.EntityScope
3031
}
3132

3233
// NewVulnReportQueryBuilder builds a query builder to build scope and cve filtering queries for vuln reporting
33-
func NewVulnReportQueryBuilder(collection *storage.ResourceCollection, vulnFilters *storage.VulnerabilityReportFilters,
34+
func NewVulnReportQueryBuilder(collection *storage.ResourceCollection, entityScope *storage.EntityScope, vulnFilters *storage.VulnerabilityReportFilters,
3435
collectionQueryRes collectionDataStore.QueryResolver, dataStartTime time.Time) *queryBuilder {
3536
return &queryBuilder{
36-
vulnFilters: vulnFilters,
3737
collection: collection,
38+
entityScope: entityScope,
39+
vulnFilters: vulnFilters,
3840
collectionQueryResolver: collectionQueryRes,
3941
dataStartTime: dataStartTime,
4042
}
43+
4144
}
4245

4346
// BuildQuery builds scope and cve filtering queries for vuln reporting
@@ -46,7 +49,17 @@ func (q *queryBuilder) BuildQuery(
4649
clusters []effectiveaccessscope.Cluster,
4750
namespaces []effectiveaccessscope.Namespace,
4851
) (*ReportQuery, error) {
49-
deploymentsQuery, err := q.collectionQueryResolver.ResolveCollectionQuery(ctx, q.collection)
52+
deploymentsQuery := search.MatchNoneQuery()
53+
var err error
54+
if q.collection != nil {
55+
deploymentsQuery, err = q.collectionQueryResolver.ResolveCollectionQuery(ctx, q.collection)
56+
} else if q.entityScope != nil {
57+
entityScopeQueryString, err := q.buildEntityScopeQueryString()
58+
if err != nil {
59+
return nil, err
60+
}
61+
deploymentsQuery, err = search.ParseQuery(entityScopeQueryString, search.MatchAllIfEmpty())
62+
}
5063
if err != nil {
5164
return nil, err
5265
}
@@ -66,7 +79,112 @@ func (q *queryBuilder) BuildQuery(
6679
}, nil
6780
}
6881

82+
func (q *queryBuilder) buildEntityScopeQueryString() (string, error) {
83+
rules := q.entityScope.GetRules()
84+
if len(rules) == 0 {
85+
return "", nil
86+
}
87+
88+
var conjuncts []string
89+
for _, rule := range rules {
90+
fieldLabel, err := entityScopeRuleToFieldLabel(rule)
91+
if err != nil {
92+
return "", err
93+
}
94+
isLabel := fieldLabel == search.DeploymentLabel ||
95+
fieldLabel == search.NamespaceLabel ||
96+
fieldLabel == search.ClusterLabel
97+
98+
values := make([]string, 0, len(rule.GetValues()))
99+
for _, rv := range rule.GetValues() {
100+
val := rv.GetValue()
101+
if rv.GetMatchType() == storage.MatchType_REGEX {
102+
val = search.RegexPrefix + val
103+
}
104+
values = append(values, val)
105+
}
106+
107+
if len(values) == 0 {
108+
continue
109+
}
110+
111+
var qb *search.QueryBuilder
112+
if isLabel {
113+
for _, v := range values {
114+
key, value := splitLabelValue(v)
115+
qb = search.NewQueryBuilder().AddMapQuery(fieldLabel, key, value)
116+
conjuncts = append(conjuncts, qb.Query())
117+
}
118+
} else if rule.GetValues()[0].GetMatchType() == storage.MatchType_REGEX {
119+
qb = search.NewQueryBuilder().AddStrings(fieldLabel, values...)
120+
conjuncts = append(conjuncts, qb.Query())
121+
} else {
122+
qb = search.NewQueryBuilder().AddExactMatches(fieldLabel, values...)
123+
conjuncts = append(conjuncts, qb.Query())
124+
}
125+
}
126+
127+
return strings.Join(conjuncts, "+"), nil
128+
}
129+
130+
func entityScopeRuleToFieldLabel(rule *storage.EntityScopeRule) (search.FieldLabel, error) {
131+
switch rule.GetEntity() {
132+
case storage.EntityType_ENTITY_TYPE_DEPLOYMENT:
133+
switch rule.GetField() {
134+
case storage.EntityField_FIELD_NAME:
135+
return search.DeploymentName, nil
136+
case storage.EntityField_FIELD_LABEL:
137+
return search.DeploymentLabel, nil
138+
case storage.EntityField_FIELD_ANNOTATION:
139+
return search.DeploymentAnnotation, nil
140+
}
141+
case storage.EntityType_ENTITY_TYPE_NAMESPACE:
142+
switch rule.GetField() {
143+
case storage.EntityField_FIELD_NAME:
144+
return search.Namespace, nil
145+
case storage.EntityField_FIELD_LABEL:
146+
return search.NamespaceLabel, nil
147+
case storage.EntityField_FIELD_ANNOTATION:
148+
return search.NamespaceAnnotation, nil
149+
}
150+
case storage.EntityType_ENTITY_TYPE_CLUSTER:
151+
switch rule.GetField() {
152+
case storage.EntityField_FIELD_NAME:
153+
return search.Cluster, nil
154+
case storage.EntityField_FIELD_LABEL:
155+
return search.ClusterLabel, nil
156+
}
157+
}
158+
return "", fmt.Errorf("unsupported entity/field combination: %s/%s", rule.GetEntity(), rule.GetField())
159+
}
160+
161+
func splitLabelValue(labelVal string) (string, string) {
162+
parts := strings.SplitN(labelVal, "=", 2)
163+
if len(parts) == 2 {
164+
return fmt.Sprintf("%q", parts[0]), fmt.Sprintf("%q", parts[1])
165+
}
166+
return fmt.Sprintf("%q", labelVal), fmt.Sprintf("%q", "")
167+
}
168+
69169
func (q *queryBuilder) buildCVEAttributesQuery() (string, error) {
170+
171+
vulnReportFilters := q.vulnFilters
172+
var conjuncts []string
173+
if q.collection != nil {
174+
conjuncts = q.addSeverityFixabilityFiltersCollectionScopedReports()
175+
} else if q.entityScope != nil {
176+
conjuncts = append(conjuncts, q.vulnFilters.GetQuery())
177+
}
178+
if filterVulnsByFirstOccurrenceTime(vulnReportFilters) {
179+
startTimeStr := fmt.Sprintf(">=%s", q.dataStartTime.Format("01/02/2006 3:04:05 PM MST"))
180+
tsQ := search.NewQueryBuilder().AddStrings(search.FirstImageOccurrenceTimestamp, startTimeStr).Query()
181+
conjuncts = append(conjuncts, tsQ)
182+
}
183+
return strings.Join(conjuncts, "+"), nil
184+
}
185+
186+
func (q *queryBuilder) addSeverityFixabilityFiltersCollectionScopedReports() []string {
187+
70188
vulnReportFilters := q.vulnFilters
71189
var conjuncts []string
72190

@@ -86,14 +204,7 @@ func (q *queryBuilder) buildCVEAttributesQuery() (string, error) {
86204
if len(severities) > 0 {
87205
conjuncts = append(conjuncts, search.NewQueryBuilder().AddExactMatches(search.Severity, severities...).Query())
88206
}
89-
90-
if filterVulnsByFirstOccurrenceTime(vulnReportFilters) {
91-
startTimeStr := fmt.Sprintf(">=%s", q.dataStartTime.Format("01/02/2006 3:04:05 PM MST"))
92-
tsQ := search.NewQueryBuilder().AddStrings(search.FirstImageOccurrenceTimestamp, startTimeStr).Query()
93-
conjuncts = append(conjuncts, tsQ)
94-
}
95-
96-
return strings.Join(conjuncts, "+"), nil
207+
return conjuncts
97208
}
98209

99210
func (q *queryBuilder) buildAccessScopeQuery(

central/reports/common/query_builder_test.go

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

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, nil, 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)