Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BED-4726 added new ~eq predicate for filtering on strings #808

Merged
merged 12 commits into from
Aug 29, 2024
Merged
21 changes: 21 additions & 0 deletions cmd/api/src/database/migration/migrations/v5.15.1.sql
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be v5.16.0 correct? Or is the goal to have this in the upcoming release?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably cutting it close to the next release since we're about to cut the RC.
I'll throw it in for v5.16.0

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- Copyright 2024 Specter Ops, Inc.
--
-- Licensed under the Apache License, Version 2.0
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- SPDX-License-Identifier: Apache-2.0


CREATE EXTENSION IF NOT EXISTS pg_trgm;

CREATE INDEX IF NOT EXISTS idx_saved_queries_description ON saved_queries using gin(description gin_trgm_ops);
CREATE INDEX IF NOT EXISTS idx_saved_queries_name ON saved_queries USING gin(name gin_trgm_ops);
10 changes: 9 additions & 1 deletion cmd/api/src/model/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ const (
LessThanOrEquals FilterOperator = "lte"
Equals FilterOperator = "eq"
NotEquals FilterOperator = "neq"
ApproximatelyEquals FilterOperator = "~eq"

GreaterThanSymbol string = ">"
GreaterThanOrEqualsSymbol string = ">="
LessThanSymbol string = "<"
LessThanOrEqualsSymbol string = "<="
EqualsSymbol string = "="
NotEqualsSymbol string = "<>"
ApproximatelyEqualSymbol string = "ILIKE"
Copy link
Contributor Author

@mvlipka mvlipka Aug 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ILIKE allows for case insensitive search

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ILIKE it!


TrueString = "true"
FalseString = "false"
Expand Down Expand Up @@ -79,6 +81,9 @@ func ParseFilterOperator(raw string) (FilterOperator, error) {
case NotEquals:
return NotEquals, nil

case ApproximatelyEquals:
return ApproximatelyEquals, nil

default:
return "", fmt.Errorf("unknown query parameter filter predicate: %s", raw)
}
Expand Down Expand Up @@ -165,6 +170,9 @@ func (s QueryParameterFilterMap) BuildSQLFilter() (SQLFilter, error) {
predicate = EqualsSymbol
case NotEquals:
predicate = NotEqualsSymbol
case ApproximatelyEquals:
predicate = ApproximatelyEqualSymbol
filter.Value = fmt.Sprintf("%%%s%%", filter.Value)
default:
return SQLFilter{}, fmt.Errorf("invalid filter predicate specified")
}
Expand Down Expand Up @@ -344,6 +352,6 @@ func (s QueryParameterFilterParser) ParseQueryParameterFilters(request *http.Req

func NewQueryParameterFilterParser() QueryParameterFilterParser {
return QueryParameterFilterParser{
re: regexp.MustCompile(`([\w]+):([\w\d\--_]+)`),
re: regexp.MustCompile(`([~\w]+):([\w\--_ ]+)`),
mistahj67 marked this conversation as resolved.
Show resolved Hide resolved
}
}
27 changes: 18 additions & 9 deletions cmd/api/src/model/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,28 @@ func TestModel_BuildSQLFilter_Success(t *testing.T) {
IsStringData: false,
}

approximatelyEquals := model.QueryParameterFilter{
Name: "filtercolumn6",
Operator: model.ApproximatelyEquals,
Value: "testing value",
IsStringData: true,
}

expectedResults := map[string]model.SQLFilter{
"numericMin": {SQLString: fmt.Sprintf("%s > ?", numericMin.Name), Params: []any{numericMin.Value}},
"numericMax": {SQLString: fmt.Sprintf("%s < ?", numericMax.Name), Params: []any{numericMax.Value}},
"stringValue": {SQLString: fmt.Sprintf("%s = ?", stringValue.Name), Params: []any{stringValue.Value}},
"boolEquals": {SQLString: fmt.Sprintf("%s = ?", boolEquals.Name), Params: []any{boolEquals.Value}},
"boolNotEquals": {SQLString: fmt.Sprintf("%s <> ?", boolNotEquals.Name), Params: []any{boolNotEquals.Value}},
"numericMin": {SQLString: fmt.Sprintf("%s > ?", numericMin.Name), Params: []any{numericMin.Value}},
"numericMax": {SQLString: fmt.Sprintf("%s < ?", numericMax.Name), Params: []any{numericMax.Value}},
"stringValue": {SQLString: fmt.Sprintf("%s = ?", stringValue.Name), Params: []any{stringValue.Value}},
"boolEquals": {SQLString: fmt.Sprintf("%s = ?", boolEquals.Name), Params: []any{boolEquals.Value}},
"boolNotEquals": {SQLString: fmt.Sprintf("%s <> ?", boolNotEquals.Name), Params: []any{boolNotEquals.Value}},
"stringApproximatelyEquals": {SQLString: fmt.Sprintf("%s ILIKE ?", approximatelyEquals.Name), Params: []any{approximatelyEquals.Value}},
}

queryParameterFilterMap := model.QueryParameterFilterMap{
numericMax.Name: model.QueryParameterFilters{numericMin, numericMax},
stringValue.Name: model.QueryParameterFilters{stringValue},
boolEquals.Name: model.QueryParameterFilters{boolEquals},
boolNotEquals.Name: model.QueryParameterFilters{boolNotEquals},
numericMax.Name: model.QueryParameterFilters{numericMin, numericMax},
stringValue.Name: model.QueryParameterFilters{stringValue},
boolEquals.Name: model.QueryParameterFilters{boolEquals},
boolNotEquals.Name: model.QueryParameterFilters{boolNotEquals},
approximatelyEquals.Name: model.QueryParameterFilters{approximatelyEquals},
}

result, err := queryParameterFilterMap.BuildSQLFilter()
Expand Down
4 changes: 2 additions & 2 deletions cmd/api/src/model/saved_queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ func (s SavedQueries) IsSortable(column string) bool {
func (s SavedQueries) ValidFilters() map[string][]FilterOperator {
return map[string][]FilterOperator{
"user_id": {Equals, NotEquals},
"name": {Equals, NotEquals},
"name": {Equals, NotEquals, ApproximatelyEquals},
"query": {Equals, NotEquals},
mistahj67 marked this conversation as resolved.
Show resolved Hide resolved
"description": {Equals, NotEquals},
"description": {Equals, NotEquals, ApproximatelyEquals},
}
}

Expand Down
28 changes: 22 additions & 6 deletions cmd/api/src/model/saved_queries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package model_test

import (
"github.com/stretchr/testify/assert"
"testing"

"github.com/specterops/bloodhound/src/model"
Expand All @@ -38,27 +39,42 @@ func TestSavedQueries_ValidFilters(t *testing.T) {
validFilters := savedQueries.ValidFilters()
require.Equal(t, 4, len(validFilters))

for _, column := range []string{"user_id", "name", "query", "description"} {
for _, column := range []string{"user_id", "query"} {
operators, ok := validFilters[column]
require.True(t, ok)
require.Equal(t, 2, len(operators))
assert.Equal(t, 2, len(operators))
}

for _, column := range []string{"name", "description"} {
operators, ok := validFilters[column]
require.True(t, ok)
assert.Equal(t, 3, len(operators))
}
}

func TestSavedQueries_GetValidFilterPredicatesAsStrings(t *testing.T) {
savedQueries := model.SavedQueries{}
for _, column := range []string{"user_id", "name", "query"} {
for _, column := range []string{"user_id", "query"} {
predicates, err := savedQueries.GetValidFilterPredicatesAsStrings(column)
require.Nil(t, err)
require.Equal(t, 2, len(predicates))
require.True(t, utils.Contains(predicates, string(model.Equals)))
require.True(t, utils.Contains(predicates, string(model.NotEquals)))
assert.True(t, utils.Contains(predicates, string(model.Equals)))
assert.True(t, utils.Contains(predicates, string(model.NotEquals)))
}

for _, column := range []string{"name", "description"} {
predicates, err := savedQueries.GetValidFilterPredicatesAsStrings(column)
require.Nil(t, err)
require.Equal(t, 3, len(predicates))
assert.True(t, utils.Contains(predicates, string(model.Equals)))
assert.True(t, utils.Contains(predicates, string(model.NotEquals)))
assert.True(t, utils.Contains(predicates, string(model.ApproximatelyEquals)))
}
}

func TestSavedQueries_IsString(t *testing.T) {
savedQueries := model.SavedQueries{}
for _, column := range []string{"name", "query"} {
for _, column := range []string{"name", "query", "description"} {
require.True(t, savedQueries.IsString(column))
}
}
1 change: 0 additions & 1 deletion packages/go/graphschema/ad/ad.go

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

1 change: 0 additions & 1 deletion packages/go/graphschema/azure/azure.go

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

1 change: 0 additions & 1 deletion packages/go/graphschema/common/common.go

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

2 changes: 1 addition & 1 deletion packages/go/openapi/doc/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -13929,7 +13929,7 @@
},
"api.params.predicate.filter.string": {
"type": "string",
"description": "Filter results by column string value. Valid filter predicates are `eq`, `neq`.\n"
"description": "Filter results by column string value. Valid filter predicates are `eq`, `neq`, `~eq`.\n"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit but should ~eq go before ne 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see that, it's related to eq so would be nice to have it read like a cascading
"equals, approximately equals, not equals"

},
"api.params.predicate.filter.integer": {
"type": "integer",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@

type: string
description: |
Filter results by column string value. Valid filter predicates are `eq`, `neq`.
Filter results by column string value. Valid filter predicates are `eq`, `neq`, `~eq`.
Loading