From c0479c8864812e44614a904827f4d88697923588 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Fri, 20 Dec 2024 17:23:10 +0100 Subject: [PATCH] feat(filter): Intersects operator The intersects operator and its case-insensitive variant, operate on strings slices. If all elements in the RHS slice are present in the slice given by LHS, the operator evaluates to true. Otherwise, it evaluates to false. --- pkg/filter/filter_test.go | 6 ++++ pkg/filter/ql/ast.go | 13 ++++++++ pkg/filter/ql/token.go | 8 +++-- pkg/util/sets/intersection.go | 46 ++++++++++++++++++++++++++ pkg/util/sets/intersection_test.go | 53 ++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 pkg/util/sets/intersection.go create mode 100644 pkg/util/sets/intersection_test.go diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index aae29344f..ad1af6048 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -153,6 +153,7 @@ func TestProcFilter(t *testing.T) { Username: "SYSTEM", Domain: "NT AUTHORITY", SID: "S-1-5-18", + Args: []string{"-k", "DcomLaunch", "-p", "-s", "LSM"}, Envs: map[string]string{"ALLUSERSPROFILE": "C:\\ProgramData", "OS": "Windows_NT", "ProgramFiles(x86)": "C:\\Program Files (x86)"}, Modules: []pstypes.Module{ {Name: "C:\\Windows\\System32\\kernel32.dll", Size: 12354, Checksum: 23123343, BaseAddress: va.Address(4294066175), DefaultBaseAddress: va.Address(4293993725)}, @@ -245,6 +246,11 @@ func TestProcFilter(t *testing.T) { {`ps.ancestor[any].name contains ('Sys')`, true}, {`ps.ancestor[any].name icontains ('sys')`, true}, {`ps.ancestor[any].pid in (2034, 343)`, true}, + + {`ps.args intersects ('-k', 'DcomLaunch')`, true}, + {`ps.args intersects ('-w', 'DcomLaunch')`, false}, + {`ps.args iintersects ('-K', 'DComLaunch')`, true}, + {`ps.args iintersects ('-W', 'DcomLaunch')`, false}, } psnap := new(ps.SnapshotterMock) diff --git a/pkg/filter/ql/ast.go b/pkg/filter/ql/ast.go index 348201e3c..98ce1a0ed 100644 --- a/pkg/filter/ql/ast.go +++ b/pkg/filter/ql/ast.go @@ -22,6 +22,7 @@ package ql import ( fuzzysearch "github.com/lithammer/fuzzysearch/fuzzy" + "github.com/rabbitstack/fibratus/pkg/util/sets" "github.com/rabbitstack/fibratus/pkg/util/wildcard" "net" "strconv" @@ -1152,6 +1153,18 @@ func (v *ValuerEval) evalBinaryExpr(expr *BinaryExpr) interface{} { } } return false + case Intersects: + rhs, ok := rhs.([]string) + if !ok { + return false + } + return len(sets.IntersectionStrings(lhs, rhs, false)) == len(rhs) + case IIntersects: + rhs, ok := rhs.([]string) + if !ok { + return false + } + return len(sets.IntersectionStrings(lhs, rhs, true)) == len(rhs) } } diff --git a/pkg/filter/ql/token.go b/pkg/filter/ql/token.go index 93f139e8f..211256bc3 100644 --- a/pkg/filter/ql/token.go +++ b/pkg/filter/ql/token.go @@ -63,6 +63,8 @@ const ( IFuzzy // ifuzzy Fuzzynorm // fuzzynorm IFuzzynorm // ifuzzynorm + Intersects // intersects + IIntersects // iintersects Eq // = IEq // ~= Neq // != @@ -91,7 +93,7 @@ func init() { for _, tok := range []token{And, Or, Contains, IContains, In, IIn, Not, Startswith, IStartswith, Endswith, IEndswith, Matches, IMatches, Fuzzy, IFuzzy, Fuzzynorm, IFuzzynorm, - Seq, MaxSpan, By, As} { + Intersects, IIntersects, Seq, MaxSpan, By, As} { keywords[strings.ToLower(tokens[tok])] = tok } keywords["true"] = True @@ -134,6 +136,8 @@ var tokens = [...]string{ IFuzzy: "IFUZZY", Fuzzynorm: "FUZZYNORM", IFuzzynorm: "IFUZZYNORM", + Intersects: "INTERSECTS", + IIntersects: "IINTERSECTS", Eq: "=", IEq: "~=", @@ -178,7 +182,7 @@ func (tok token) precedence() int { case Eq, IEq, Neq, Lt, Lte, Gt, Gte: return 4 case In, IIn, Contains, IContains, Startswith, IStartswith, Endswith, IEndswith, - Matches, IMatches, Fuzzy, IFuzzy, Fuzzynorm, IFuzzynorm: + Matches, IMatches, Fuzzy, IFuzzy, Fuzzynorm, IFuzzynorm, Intersects, IIntersects: return 5 } return 0 diff --git a/pkg/util/sets/intersection.go b/pkg/util/sets/intersection.go new file mode 100644 index 000000000..b9a8bdcbb --- /dev/null +++ b/pkg/util/sets/intersection.go @@ -0,0 +1,46 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * 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. + */ + +package sets + +import "strings" + +// IntersectionStrings computes the intersection of two +// string slices. The boolean argument specifies if the +// string comparison is case-sensitive or not. +func IntersectionStrings(s1, s2 []string, ignoreCase bool) []string { + inter := make([]string, 0) + bucket := map[string]bool{} + + for _, i := range s1 { + for _, j := range s2 { + var eq bool + if ignoreCase { + eq = strings.EqualFold(i, j) && !bucket[i] + } else { + eq = i == j && !bucket[i] + } + if eq { + inter = append(inter, i) + bucket[i] = true + } + } + } + + return inter +} diff --git a/pkg/util/sets/intersection_test.go b/pkg/util/sets/intersection_test.go new file mode 100644 index 000000000..d2ba2172c --- /dev/null +++ b/pkg/util/sets/intersection_test.go @@ -0,0 +1,53 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * 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. + */ + +package sets + +import ( + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +func TestIntersectionStrings(t *testing.T) { + var tests = []struct { + s1 []string + s2 []string + ignoreCase bool + in []string + }{ + { + []string{"-k", "DcomLaunch", "-p", "-s", "LSM"}, []string{"DcomLaunch", "-s"}, true, []string{"DcomLaunch", "-s"}, + }, + { + []string{"-k", "DcomLaunch", "-p", "-s", "LSM"}, []string{"DComLaunch", "-s"}, false, []string{"-s"}, + }, + { + []string{"-k", "DcomLaunch", "-p", "-s", "LSM"}, []string{"LocalSystemNetworkRestricted"}, true, []string{}, + }, + { + []string{"LSM", "-s"}, []string{"-S", "lsm"}, true, []string{"LSM", "-s"}, + }, + } + + for _, tt := range tests { + t.Run(strings.Join(tt.in, ","), func(t *testing.T) { + assert.Equal(t, tt.in, IntersectionStrings(tt.s1, tt.s2, tt.ignoreCase)) + }) + } +}