Skip to content
Draft
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
109 changes: 104 additions & 5 deletions central/reports/common/query_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,20 @@ type queryBuilder struct {
collection *storage.ResourceCollection
collectionQueryResolver collectionDataStore.QueryResolver
dataStartTime time.Time
entityScope *storage.EntityScope
}

// NewVulnReportQueryBuilder builds a query builder to build scope and cve filtering queries for vuln reporting
func NewVulnReportQueryBuilder(collection *storage.ResourceCollection, vulnFilters *storage.VulnerabilityReportFilters,
func NewVulnReportQueryBuilder(collection *storage.ResourceCollection, entityScope *storage.EntityScope, vulnFilters *storage.VulnerabilityReportFilters,
collectionQueryRes collectionDataStore.QueryResolver, dataStartTime time.Time) *queryBuilder {
return &queryBuilder{
vulnFilters: vulnFilters,
collection: collection,
entityScope: entityScope,
vulnFilters: vulnFilters,
collectionQueryResolver: collectionQueryRes,
dataStartTime: dataStartTime,
}

}

// BuildQuery builds scope and cve filtering queries for vuln reporting
Expand All @@ -46,10 +49,22 @@ func (q *queryBuilder) BuildQuery(
clusters []effectiveaccessscope.Cluster,
namespaces []effectiveaccessscope.Namespace,
) (*ReportQuery, error) {
deploymentsQuery, err := q.collectionQueryResolver.ResolveCollectionQuery(ctx, q.collection)
if err != nil {
return nil, err
deploymentsQuery := search.MatchNoneQuery()
var err error
if q.collection != nil {
deploymentsQuery, err = q.collectionQueryResolver.ResolveCollectionQuery(ctx, q.collection)
if err != nil {
return nil, err
}
} else {

entityScopeQueryString, err := q.buildEntityScopeQueryString()
if err != nil {
return nil, err
}
deploymentsQuery, err = search.ParseQuery(entityScopeQueryString, search.MatchAllIfEmpty())
}

scopeQuery, err := q.buildAccessScopeQuery(clusters, namespaces)
if err != nil {
return nil, err
Expand All @@ -66,6 +81,90 @@ func (q *queryBuilder) BuildQuery(
}, nil
}

func (q *queryBuilder) buildEntityScopeQueryString() (string, error) {
rules := q.entityScope.GetRules()
if len(rules) == 0 {
return "", nil
}

var conjuncts []string
for _, rule := range rules {
fieldLabel, err := entityScopeRuleToFieldLabel(rule)
if err != nil {
return "", err
}
isLabel := fieldLabel == search.DeploymentLabel ||
fieldLabel == search.NamespaceLabel ||
fieldLabel == search.ClusterLabel

values := make([]string, 0, len(rule.GetValues()))
for _, rv := range rule.GetValues() {
val := rv.GetValue()
if rv.GetMatchType() == storage.MatchType_REGEX {
val = search.RegexPrefix + val
}
values = append(values, val)
}

if len(values) == 0 {
continue
}

var qb *search.QueryBuilder
if isLabel {
for _, v := range values {
key, value := splitLabelValue(v)
qb = search.NewQueryBuilder().AddMapQuery(fieldLabel, key, value)
conjuncts = append(conjuncts, qb.Query())
}
} else {
qb = search.NewQueryBuilder().AddExactMatches(fieldLabel, values...)
conjuncts = append(conjuncts, qb.Query())
}
}

return strings.Join(conjuncts, "+"), nil
}

func entityScopeRuleToFieldLabel(rule *storage.EntityScopeRule) (search.FieldLabel, error) {
switch rule.GetEntity() {
case storage.EntityType_ENTITY_TYPE_DEPLOYMENT:
switch rule.GetField() {
case storage.EntityField_FIELD_NAME:
return search.DeploymentName, nil
case storage.EntityField_FIELD_LABEL:
return search.DeploymentLabel, nil
case storage.EntityField_FIELD_ANNOTATION:
return search.DeploymentAnnotation, nil
}
case storage.EntityType_ENTITY_TYPE_NAMESPACE:
switch rule.GetField() {
case storage.EntityField_FIELD_NAME:
return search.Namespace, nil
case storage.EntityField_FIELD_LABEL:
return search.NamespaceLabel, nil
case storage.EntityField_FIELD_ANNOTATION:
return search.NamespaceAnnotation, nil
}
case storage.EntityType_ENTITY_TYPE_CLUSTER:
switch rule.GetField() {
case storage.EntityField_FIELD_NAME:
return search.Cluster, nil
case storage.EntityField_FIELD_LABEL:
return search.ClusterLabel, nil
}
}
return "", fmt.Errorf("unsupported entity/field combination: %s/%s", rule.GetEntity(), rule.GetField())
}

func splitLabelValue(labelVal string) (string, string) {
parts := strings.SplitN(labelVal, "=", 2)
if len(parts) == 2 {
return fmt.Sprintf("%q", parts[0]), fmt.Sprintf("%q", parts[1])
}
return fmt.Sprintf("%q", labelVal), fmt.Sprintf("%q", "")
}

func (q *queryBuilder) buildCVEAttributesQuery() (string, error) {
vulnReportFilters := q.vulnFilters
var conjuncts []string
Expand Down
175 changes: 175 additions & 0 deletions central/reports/common/query_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,178 @@ func TestBuildAccessScopeQuery(t *testing.T) {
func assertByDirectComparison(t testing.TB, expected *v1.Query, actual *v1.Query) {
protoassert.Equal(t, expected, actual)
}

func TestBuildEntityScopeQueryString(t *testing.T) {
testCases := []struct {
name string
scope *storage.EntityScope
expected string
hasError bool
}{
{
name: "Empty rules returns empty string (match all)",
scope: &storage.EntityScope{},
expected: "",
},
{
name: "Single namespace name rule",
scope: &storage.EntityScope{
Rules: []*storage.EntityScopeRule{
{
Entity: storage.EntityType_ENTITY_TYPE_NAMESPACE,
Field: storage.EntityField_FIELD_NAME,
Values: []*storage.RuleValue{
{Value: "prod", MatchType: storage.MatchType_EXACT},
{Value: "staging", MatchType: storage.MatchType_EXACT},
},
},
},
},
expected: search.NewQueryBuilder().AddExactMatches(search.Namespace, "prod", "staging").Query(),
},
{
name: "Single deployment name rule",
scope: &storage.EntityScope{
Rules: []*storage.EntityScopeRule{
{
Entity: storage.EntityType_ENTITY_TYPE_DEPLOYMENT,
Field: storage.EntityField_FIELD_NAME,
Values: []*storage.RuleValue{
{Value: "web-server", MatchType: storage.MatchType_EXACT},
},
},
},
},
expected: search.NewQueryBuilder().AddExactMatches(search.DeploymentName, "web-server").Query(),
},
{
name: "Single cluster name rule",
scope: &storage.EntityScope{
Rules: []*storage.EntityScopeRule{
{
Entity: storage.EntityType_ENTITY_TYPE_CLUSTER,
Field: storage.EntityField_FIELD_NAME,
Values: []*storage.RuleValue{
{Value: "prod-us", MatchType: storage.MatchType_EXACT},
{Value: "prod-eu", MatchType: storage.MatchType_EXACT},
},
},
},
},
expected: search.NewQueryBuilder().AddExactMatches(search.Cluster, "prod-us", "prod-eu").Query(),
},
{
name: "Multiple rules are ANDed",
scope: &storage.EntityScope{
Rules: []*storage.EntityScopeRule{
{
Entity: storage.EntityType_ENTITY_TYPE_NAMESPACE,
Field: storage.EntityField_FIELD_NAME,
Values: []*storage.RuleValue{
{Value: "prod", MatchType: storage.MatchType_EXACT},
},
},
{
Entity: storage.EntityType_ENTITY_TYPE_DEPLOYMENT,
Field: storage.EntityField_FIELD_NAME,
Values: []*storage.RuleValue{
{Value: "backend", MatchType: storage.MatchType_EXACT},
{Value: "frontend", MatchType: storage.MatchType_EXACT},
},
},
},
},
expected: search.NewQueryBuilder().AddExactMatches(search.Namespace, "prod").Query() +
"+" +
search.NewQueryBuilder().AddExactMatches(search.DeploymentName, "backend", "frontend").Query(),
},
{
name: "Label rule uses map query",
scope: &storage.EntityScope{
Rules: []*storage.EntityScopeRule{
{
Entity: storage.EntityType_ENTITY_TYPE_NAMESPACE,
Field: storage.EntityField_FIELD_LABEL,
Values: []*storage.RuleValue{
{Value: "env=prod", MatchType: storage.MatchType_EXACT},
},
},
},
},
expected: search.NewQueryBuilder().AddMapQuery(search.NamespaceLabel, `"env"`, `"prod"`).Query(),
},
{
name: "Regex match type adds r/ prefix",
scope: &storage.EntityScope{
Rules: []*storage.EntityScopeRule{
{
Entity: storage.EntityType_ENTITY_TYPE_DEPLOYMENT,
Field: storage.EntityField_FIELD_NAME,
Values: []*storage.RuleValue{
{Value: "web-.*", MatchType: storage.MatchType_REGEX},
},
},
},
},
expected: search.NewQueryBuilder().AddExactMatches(search.DeploymentName, "r/web-.*").Query(),
},
{
name: "Rule with empty values is skipped",
scope: &storage.EntityScope{
Rules: []*storage.EntityScopeRule{
{
Entity: storage.EntityType_ENTITY_TYPE_NAMESPACE,
Field: storage.EntityField_FIELD_NAME,
Values: []*storage.RuleValue{},
},
},
},
expected: "",
},
{
name: "Unsupported entity/field returns error",
scope: &storage.EntityScope{
Rules: []*storage.EntityScopeRule{
{
Entity: storage.EntityType_ENTITY_TYPE_CLUSTER,
Field: storage.EntityField_FIELD_ANNOTATION,
Values: []*storage.RuleValue{
{Value: "team=infra", MatchType: storage.MatchType_EXACT},
},
},
},
},
hasError: true,
},
{
name: "Deployment annotation rule",
scope: &storage.EntityScope{
Rules: []*storage.EntityScopeRule{
{
Entity: storage.EntityType_ENTITY_TYPE_DEPLOYMENT,
Field: storage.EntityField_FIELD_ANNOTATION,
Values: []*storage.RuleValue{
{Value: "owner=team-a", MatchType: storage.MatchType_EXACT},
},
},
},
},
expected: search.NewQueryBuilder().AddExactMatches(search.DeploymentAnnotation, "owner=team-a").Query(),
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
qb := &queryBuilder{
entityScope: tc.scope,
}
result, err := qb.buildEntityScopeQueryString()
if tc.hasError {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tc.expected, result)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ func (rg *reportGeneratorImpl) buildReportQueryViewBased(snap *storage.ReportSna

func (rg *reportGeneratorImpl) buildReportQuery(snap *storage.ReportSnapshot,
collection *storage.ResourceCollection, dataStartTime time.Time) (*common.ReportQuery, error) {
qb := common.NewVulnReportQueryBuilder(collection, snap.GetVulnReportFilters(), rg.collectionQueryResolver,
qb := common.NewVulnReportQueryBuilder(collection, snap.GetResourceScope().GetEntityScope(), snap.GetVulnReportFilters(), rg.collectionQueryResolver,
dataStartTime)
allClusters, allNamespaces, err := rg.getClustersAndNamespacesForSAC()
if err != nil {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading