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

Feature: add process tree data source #3488

Closed
Closed
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
7 changes: 6 additions & 1 deletion pkg/ebpf/signature_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/aquasecurity/tracee/pkg/containers"
"github.com/aquasecurity/tracee/pkg/events"
"github.com/aquasecurity/tracee/pkg/logger"
"github.com/aquasecurity/tracee/pkg/proctree"
"github.com/aquasecurity/tracee/pkg/signatures/engine"
"github.com/aquasecurity/tracee/types/detect"
"github.com/aquasecurity/tracee/types/protocol"
Expand Down Expand Up @@ -119,7 +120,11 @@ func (t *Tracee) engineEvents(ctx context.Context, in <-chan *trace.Event) (<-ch

// PrepareBuiltinDataSources returns a list of all data sources tracee makes available built-in
func (t *Tracee) PrepareBuiltinDataSources() []detect.DataSource {
return []detect.DataSource{
datasources := []detect.DataSource{
containers.NewDataSource(t.containers),
}
if t.config.ProcTree.Enabled {
datasources = append(datasources, proctree.NewDataSource(t.processTree))
}
return datasources
}
198 changes: 198 additions & 0 deletions pkg/proctree/datasource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package proctree

import (
"encoding/json"
"time"

"github.com/aquasecurity/tracee/types/datasource"
"github.com/aquasecurity/tracee/types/detect"
)

// DataSource is an implementation to detect.Datasource interface, enveloping the ProcessTree type.
// It exposes all the relevant information of the tree using the interface methods.
type DataSource struct {
procTree *ProcessTree
}

func NewDataSource(processTree *ProcessTree) *DataSource {
return &DataSource{procTree: processTree}
}

func (ptds *DataSource) Get(key interface{}) (map[string]interface{}, error) {
switch typedKey := key.(type) {
case datasource.ProcKey:
process, found := ptds.procTree.GetProcessByHash(typedKey.EntityId)
if !found {
return nil, detect.ErrDataNotFound
}
return map[string]interface{}{
"process_info": ptds.exportProcessInfo(process, typedKey.Time),
}, nil
case datasource.ThreadKey:
thread, found := ptds.procTree.GetThreadByHash(typedKey.EntityId)
if !found {
return nil, detect.ErrDataNotFound
}
return map[string]interface{}{
"thread_info": ptds.exportThreadInfo(thread, typedKey.Time),
}, nil
case datasource.LineageKey:
process, found := ptds.procTree.GetProcessByHash(typedKey.EntityId)
if !found {
return nil, detect.ErrDataNotFound
}
return map[string]interface{}{
"process_lineage": ptds.exportProcessLineage(process, typedKey.Time, typedKey.MaxDepth),
}, nil
default:
return nil, detect.ErrKeyNotSupported
}
}

func (ptds *DataSource) Keys() []string {
return []string{"datasource.ProcKey", "datasource.ThreadKey", "datasource.LineageKey"}
}

func (ptds *DataSource) Schema() string {
schemaMap := map[string]string{
"process_info": "datasource.ProcessInfo",
"thread_info": "datasource.ThreadInfo",
"process_lineage": "datasource.ProcessLineage",
}
schema, _ := json.Marshal(schemaMap)
return string(schema)
}

func (ptds *DataSource) Version() uint {
return 1 // TODO: Change to semantic versioning
}

func (ptds *DataSource) Namespace() string {
return "tracee"
}

func (ptds *DataSource) ID() string {
return "process_tree"
}

// exportProcessInfo return a representation of the given Process information relevant to given time
// as the expected datasource process structure.
func (ptds *DataSource) exportProcessInfo(
process *Process,
queryTime time.Time,
) datasource.ProcessInfo {
info := process.GetInfo()
executable := process.GetExecutable()
interpreter := process.GetInterpreter()
interp := process.GetInterp()

existingChildren := make(map[int]uint32)
for _, childHash := range process.GetChildren() {
child, ok := ptds.procTree.GetProcessByHash(childHash)
if !ok {
continue
}
childInfo := child.GetInfo()
if childInfo.IsAliveAt(queryTime) {
existingChildren[childInfo.GetPid()] = childHash
}
}

existingThreads := make(map[int]uint32)
for _, threadHash := range process.GetThreads() {
thread, ok := ptds.procTree.GetThreadByHash(threadHash)
if !ok {
continue
}
threadInfo := thread.GetInfo()
if threadInfo.IsAliveAt(queryTime) {
existingThreads[threadInfo.GetPid()] = threadHash
}
}

infoFeed := info.GetFeedAt(queryTime)

return datasource.ProcessInfo{
EntityId: process.GetHash(),
Pid: infoFeed.Pid,
NsPid: infoFeed.NsPid,
Ppid: infoFeed.PPid,
ContainerId: "", // TODO: Add
Cmd: []string{}, // TODO: Add
ExecutionBinary: exportFileInfo(executable, queryTime),
Interpreter: exportFileInfo(interpreter, queryTime),
Interp: exportFileInfo(interp, queryTime),
StartTime: info.GetStartTime(),
ExecTime: time.Unix(0, 0), // TODO: Add
ExitTime: info.GetExitTime(),
ParentEntityId: process.GetParentHash(),
ThreadsIds: existingThreads,
ChildProcessesIds: existingChildren,
IsAlive: info.IsAliveAt(queryTime),
}
}

// exportThreadInfo return a representation of the given Thread information relevant to given time
// as the expected datasource thread structure.
func (ptds *DataSource) exportThreadInfo(
thread *Thread,
queryTime time.Time,
) datasource.ThreadInfo {
info := thread.GetInfo()
infoFeed := info.GetFeedAt(queryTime)
return datasource.ThreadInfo{
EntityId: thread.GetHash(),
Tid: infoFeed.Tid,
NsTid: infoFeed.NsTid,
Pid: infoFeed.Pid,
UserId: infoFeed.Uid,
GroupId: infoFeed.Gid,
StartTime: info.GetStartTime(),
ExitTime: info.GetExitTime(),
Name: infoFeed.Name,
IsAlive: info.IsAliveAt(queryTime),
}
}

// exportProcessLineage return a representation of the given Process information and up to a given
// depth of its ancestors as the expected datasource process lineage structure.
// The information of each struct is relevant to the fork time of its dependent from it.
func (ptds *DataSource) exportProcessLineage(
process *Process,
queryTime time.Time,
maxDepth int,
) datasource.ProcessLineage {
lineage := datasource.ProcessLineage{
ptds.exportProcessInfo(process, queryTime),
}
currentProcess := process
var found bool
var iterationQueryTime time.Time
for depth := 0; depth < maxDepth; depth++ {
// We don't want to get parents processes which are not part of the container.
if currentProcess.GetInfo().GetNsPid() == 1 {
break
}
iterationQueryTime = currentProcess.GetInfo().GetStartTime()
currentProcess, found = ptds.procTree.GetProcessByHash(currentProcess.GetParentHash())
if !found {
break
}
lineage = append(lineage, ptds.exportProcessInfo(currentProcess, iterationQueryTime))
}
return lineage
}

// exportFileInfo return a representation of the given FileInfo information relevant to given time
// as the expected datasource file info structure.
func exportFileInfo(fileInfo *FileInfo, queryTime time.Time) datasource.FileInfo {
fileInfoFeed := fileInfo.GetFeedAt(queryTime)
return datasource.FileInfo{
Path: fileInfoFeed.Path,
Hash: "", // TODO: Add
Inode: fileInfoFeed.Inode,
Device: fileInfoFeed.Dev,
Ctime: time.Unix(0, int64(fileInfoFeed.Ctime)),
Mode: fileInfoFeed.InodeMode,
}
}
10 changes: 5 additions & 5 deletions pkg/proctree/proctree_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (pt *ProcessTree) String() string {
if !ok {
continue
}
if child.info.HasExited() { // only running children
if !child.info.IsAlive() { // only running children
continue
}
pid := fmt.Sprintf("%d", child.GetInfo().GetPid())
Expand Down Expand Up @@ -56,7 +56,7 @@ func (pt *ProcessTree) String() string {
if !ok {
continue
}
if thread.info.HasExited() { // only running threads
if !thread.info.IsAlive() { // only running threads
continue
}
tid := fmt.Sprintf("%d", thread.GetInfo().GetTid())
Expand All @@ -79,9 +79,9 @@ func (pt *ProcessTree) String() string {
// Use tablewriter to print the tree in a table
newTable := func() *tablewriter.Table {
table := tablewriter.NewWriter(buffer)
table.SetHeader([]string{"Ppid", "Tid", "Pid", "Date", "Comm", "Children", "Threads"})
table.SetHeader([]string{"Ppid", "Tid", "Pid", "EntityId", "Date", "Comm", "Children", "Threads"})
// If debug() is enabled:
// table.SetHeader([]string{"Ppid", "Tid", "Pid", "StartTime", "Hash", "CMD", "Children", "Threads"})
// table.SetHeader([]string{"Ppid", "Tid", "Pid", "StartTime", "EntityId", "CMD", "Children", "Threads"})
table.SetAutoWrapText(false)
table.SetRowLine(false)
table.SetAutoFormatHeaders(true)
Expand All @@ -101,7 +101,7 @@ func (pt *ProcessTree) String() string {
if !ok {
continue
}
if process.info.HasExited() {
if !process.info.IsAlive() {
continue // only running processes
}

Expand Down
24 changes: 21 additions & 3 deletions pkg/proctree/taskinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,27 @@ func (ti *TaskInfo) GetExitTime() time.Time {
return bootTime.Add(duration)
}

// HasExited returns true if the task has exited.
func (ti *TaskInfo) HasExited() bool {
// IsAlive returns true if the task has exited.
func (ti *TaskInfo) IsAlive() bool {
ti.mutex.RLock()
defer ti.mutex.RUnlock()
return ti.exitTimeNS != 0
return ti.exitTimeNS == 0
}

// IsAliveAt return whether the task is alive in the given time, either because it didn't start
// yet or it has exited.
func (ti *TaskInfo) IsAliveAt(targetTime time.Time) bool {
ti.mutex.RLock()
defer ti.mutex.RUnlock()
if ti.exitTimeNS != 0 {
if targetTime.After(utils.NsSinceBootTimeToTime(ti.exitTimeNS)) {
return false
}
}
// If start time is not initialized it will count as 0 ns, meaning it will be before any
// query time given.
if targetTime.Before(utils.NsSinceBootTimeToTime(ti.startTimeNS)) {
return false
}
return true
}