Repository: rabbitstack/fibratus (language: python)

Issue:
Introduce parent process filter fields and JSON attributes
Having access to the parent process fields in filters would be a valuable addition. Filter fields should follow the following notation: `ps.parent.<field>`, where `<field>` could be one of `name`, `exe`, `comm`, `sid`, `args`, etc. The event JSON serializer should take care of squashing the parent process attributes into the result JSON object.


Discussion / hints:
Solved in #74 

Fix patch:
diff --git a/pkg/filter/accessor.go b/pkg/filter/accessor.go
index 9fc38971f..0c45c89c7 100644
--- a/pkg/filter/accessor.go
+++ b/pkg/filter/accessor.go
@@ -344,50 +344,96 @@ func (ps *psAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, e
 			}
 
 		case strings.HasPrefix(field, fields.PsParentSequence):
-			key, segment := captureInBrackets(field)
-			depth, err := strconv.Atoi(strings.TrimSpace(key))
-			if err != nil {
-				// we got the `root` key
-				depth = -1
-			}
+			return parentFields(field, kevt)
+		}
 
-			var (
-				dp int
-				ps *pstypes.PS
-			)
-			pstypes.Visit(func(proc *pstypes.PS) {
-				dp++
-				if dp == depth || depth == -1 {
-					ps = proc
-				}
-			}, kevt.PS)
+		return nil, nil
+	}
+}
 
-			if ps == nil {
-				return nil, nil
-			}
+// parentFields recursively walks the process ancestors and extracts
+// the required field values. If we get the `root` key, the root ancestor
+// fields are inspected, while `any` accumulates values of all ancestors.
+// Alternatively, the key may represent the depth that only returns the
+// ancestor located at the given depth, starting with 1 which is the immediate
+// process parent.
+func parentFields(field string, kevt *kevent.Kevent) (kparams.Value, error) {
+	key, segment := captureInBrackets(field)
+	if key == "" || segment == "" {
+		return nil, nil
+	}
+
+	var ps *pstypes.PS
 
+	switch key {
+	case "root":
+		pstypes.Walk(func(proc *pstypes.PS) {
+			ps = proc
+		}, kevt.PS)
+
+	case "any":
+		values := make([]string, 0)
+		pstypes.Walk(func(ps *pstypes.PS) {
 			switch segment {
 			case fields.ProcessName:
-				return ps.Name, nil
+				values = append(values, ps.Name)
 			case fields.ProcessID:
-				return ps.PID, nil
+				values = append(values, strconv.Itoa(int(ps.PID)))
 			case fields.ProcessSID:
-				return ps.SID, nil
+				values = append(values, ps.SID)
 			case fields.ProcessSessionID:
-				return ps.SessionID, nil
+				values = append(values, strconv.Itoa(int(ps.SessionID)))
 			case fields.ProcessCwd:
-				return ps.Cwd, nil
+				values = append(values, ps.Cwd)
 			case fields.ProcessComm:
-				return ps.Comm, nil
+				values = append(values, ps.Comm)
 			case fields.ProcessArgs:
-				return ps.Args, nil
+				values = append(values, ps.Args...)
 			case fields.ProcessExe:
-				return ps.Exe, nil
+				values = append(values, ps.Exe)
 			}
+		}, kevt.PS)
+
+		return values, nil
+
+	default:
+		depth, err := strconv.Atoi(key)
+		if err != nil {
+			return nil, err
 		}
+		var i int
+		pstypes.Walk(func(proc *pstypes.PS) {
+			i++
+			if i == depth {
+				ps = proc
+			}
+		}, kevt.PS)
+	}
 
+	if ps == nil {
 		return nil, nil
 	}
+
+	switch segment {
+	case fields.ProcessName:
+		return ps.Name, nil
+	case fields.ProcessID:
+		return ps.PID, nil
+	case fields.ProcessSID:
+		return ps.SID, nil
+	case fields.ProcessSessionID:
+		return ps.SessionID, nil
+	case fields.ProcessCwd:
+		return ps.Cwd, nil
+	case fields.ProcessComm:
+		return ps.Comm, nil
+	case fields.ProcessArgs:
+		return ps.Args, nil
+	case fields.ProcessExe:
+		return ps.Exe, nil
+	}
+
+	return nil, nil
 }
 
 // threadAccessor fetches thread parameters from thread kernel events.
diff --git a/pkg/filter/fields/fields.go b/pkg/filter/fields/fields.go
index e4c116283..91a2be9c8 100644
--- a/pkg/filter/fields/fields.go
+++ b/pkg/filter/fields/fields.go
@@ -478,8 +478,11 @@ func Lookup(name string) Field {
 		// that represents the depth of
 		// the parent process node or the
 		// `root` keyword to designate the
-		// root process node
-		var keyRegexp = regexp.MustCompile(`[1-9]+|root`)
+		// root process node. Additionally,
+		// we can also get the `any` keyword
+		// that collects the fields of all
+		// ancestor processes
+		var keyRegexp = regexp.MustCompile(`[1-9]+|root|any`)
 		if !keyRegexp.MatchString(key) {
 			return None
 		}
diff --git a/pkg/filter/ql/ast.go b/pkg/filter/ql/ast.go
index bfe456284..c2c2b48a2 100644
--- a/pkg/filter/ql/ast.go
+++ b/pkg/filter/ql/ast.go
@@ -912,16 +912,40 @@ func (v *ValuerEval) evalBinaryExpr(expr *BinaryExpr) interface{} {
 	case []string:
 		switch expr.Op {
 		case contains:
-			rhs, ok := rhs.(string)
+			s, ok := rhs.(string)
 			if !ok {
+				rhs, ok := rhs.([]string)
+				if !ok {
+					return false
+				}
+				for _, s1 := range rhs {
+					for _, s2 := range lhs {
+						if strings.Contains(s2, s1) {
+							return true
+						}
+					}
+				}
 				return false
 			}
-			for _, s := range lhs {
-				if s == rhs {
+			for _, val := range lhs {
+				if strings.Contains(val, s) {
 					return true
 				}
 			}
 			return false
+		case icontains:
+			rhs, ok := rhs.([]string)
+			if !ok {
+				return false
+			}
+			for _, s1 := range lhs {
+				for _, s2 := range rhs {
+					if strings.Contains(strings.ToLower(s1), strings.ToLower(s2)) {
+						return true
+					}
+				}
+			}
+			return false
 		case in:
 			rhs, ok := rhs.([]string)
 			if !ok {
@@ -935,6 +959,58 @@ func (v *ValuerEval) evalBinaryExpr(expr *BinaryExpr) interface{} {
 				}
 			}
 			return false
+		case startswith:
+			rhs, ok := rhs.([]string)
+			if !ok {
+				return false
+			}
+			for _, s1 := range rhs {
+				for _, s2 := range lhs {
+					if strings.HasPrefix(s2, s1) {
+						return true
+					}
+				}
+			}
+			return false
+		case istartswith:
+			rhs, ok := rhs.([]string)
+			if !ok {
+				return false
+			}
+			for _, s1 := range rhs {
+				for _, s2 := range lhs {
+					if strings.HasPrefix(strings.ToLower(s2), strings.ToLower(s1)) {
+						return true
+					}
+				}
+			}
+			return false
+		case endswith:
+			rhs, ok := rhs.([]string)
+			if !ok {
+				return false
+			}
+			for _, s1 := range rhs {
+				for _, s2 := range lhs {
+					if strings.HasSuffix(s2, s1) {
+						return true
+					}
+				}
+			}
+			return false
+		case iendswith:
+			rhs, ok := rhs.([]string)
+			if !ok {
+				return false
+			}
+			for _, s1 := range rhs {
+				for _, s2 := range lhs {
+					if strings.HasSuffix(strings.ToLower(s2), strings.ToLower(s1)) {
+						return true
+					}
+				}
+			}
+			return false
 		case matches:
 			rhs, ok := rhs.([]string)
 			if !ok {
diff --git a/pkg/ps/types/types.go b/pkg/ps/types/types.go
index c5380d46d..f0d96aa45 100644
--- a/pkg/ps/types/types.go
+++ b/pkg/ps/types/types.go
@@ -71,9 +71,9 @@ type PS struct {
 // invoked on each parent visit walk.
 type Visitor func(*PS)
 
-// Visit recursively visits all parents of the given process
+// Walk recursively visits all parents of the given process
 // and invokes the visitor function on each parent process.
-func Visit(v Visitor, ps *PS) {
+func Walk(v Visitor, ps *PS) {
 	if ps == nil {
 		return
 	}
@@ -82,7 +82,7 @@ func Visit(v Visitor, ps *PS) {
 	}
 	v(ps.Parent)
 	if ps.Parent != nil {
-		Visit(v, ps.Parent)
+		Walk(v, ps.Parent)
 	}
 }
 


Test patch:
diff --git a/pkg/filter/fields/fields_test.go b/pkg/filter/fields/fields_test.go
index 1f6dd0ffa..222f0b9fb 100644
--- a/pkg/filter/fields/fields_test.go
+++ b/pkg/filter/fields/fields_test.go
@@ -38,6 +38,7 @@ func TestLookup(t *testing.T) {
 	assert.Empty(t, Lookup("ps.pe.sections[.debug$S].e"))
 	assert.Equal(t, Field("ps.parent[1].name"), Lookup("ps.parent[1].name"))
 	assert.Equal(t, Field("ps.parent[root].name"), Lookup("ps.parent[root].name"))
+	assert.Equal(t, Field("ps.parent[any].pid"), Lookup("ps.parent[any].pid"))
 	assert.Equal(t, Field("ps.parent[2].sid"), Lookup("ps.parent[2].sid"))
 	assert.Empty(t, Lookup("ps.parent[ro].name"))
 }
diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go
index b817899c0..89021bafc 100644
--- a/pkg/filter/filter_test.go
+++ b/pkg/filter/filter_test.go
@@ -67,6 +67,7 @@ func TestFilterRunProcessKevent(t *testing.T) {
 		Parent: &pstypes.PS{
 			Name: "services.exe",
 			SID:  "administrator/SYSTEM",
+			PID:  2034,
 			Parent: &pstypes.PS{
 				Name: "System",
 			},
@@ -115,6 +116,15 @@ func TestFilterRunProcessKevent(t *testing.T) {
 		{`ps.parent[2].name = 'services.exe'`, true},
 		{`ps.parent[2].sid = 'administrator/SYSTEM'`, true},
 		{`ps.parent[root].name = 'System'`, true},
+		{`ps.parent[any].name in ('services.exe', 'System')`, true},
+		{`ps.parent[any].name not in ('svchost.exe')`, true},
+		{`ps.parent[any].name endswith ('ices.exe')`, true},
+		{`ps.parent[any].name iendswith ('TeM')`, true},
+		{`ps.parent[any].name startswith ('serv')`, true},
+		{`ps.parent[any].name istartswith ('Serv')`, true},
+		{`ps.parent[any].name contains ('Sys')`, true},
+		{`ps.parent[any].name icontains ('sys')`, true},
+		{`ps.parent[any].pid in (2034, 343)`, true},
 	}
 
 	for i, tt := range tests {
diff --git a/pkg/ps/types/types_test.go b/pkg/ps/types/types_test.go
index 1c37229b7..5ee1b0d10 100644
--- a/pkg/ps/types/types_test.go
+++ b/pkg/ps/types/types_test.go
@@ -39,7 +39,7 @@ func TestVisit(t *testing.T) {
 	expected := []string{"powershell.exe", "cmd.exe"}
 	parents := make([]string, 0)
 
-	Visit(func(ps *PS) { parents = append(parents, ps.Name) }, ps3)
+	Walk(func(ps *PS) { parents = append(parents, ps.Name) }, ps3)
 
 	assert.Equal(t, expected, parents)
 
@@ -55,7 +55,7 @@ func TestVisit(t *testing.T) {
 	expected1 := []string{"iexplorer.exe", "winword.exe", "powershell.exe", "cmd.exe"}
 	parents1 := make([]string, 0)
 
-	Visit(func(ps *PS) { parents1 = append(parents1, ps.Name) }, ps5)
+	Walk(func(ps *PS) { parents1 = append(parents1, ps.Name) }, ps5)
 
 	assert.Equal(t, expected1, parents1)
 }