diff --git a/pkg/ebpf/tracee.go b/pkg/ebpf/tracee.go index 55a632340b1c..0c84a3347485 100644 --- a/pkg/ebpf/tracee.go +++ b/pkg/ebpf/tracee.go @@ -588,7 +588,7 @@ func (t *Tracee) Init(ctx gocontext.Context) error { return t.extensions.InitExtensionsForPhase(ctx, t, InitPhaseComplete) } -// initTailCall initializes a given tailcall. +// initTailCall initializes a given tailcall by updating the BPF map with the program FD. func (t *Tracee) initTailCall(tailCall events.TailCall) error { tailCallMapName := tailCall.GetMapName() tailCallProgName := tailCall.GetProgName() @@ -626,6 +626,37 @@ func (t *Tracee) initTailCall(tailCall events.TailCall) error { return nil } +// uninitTailCall removes a given tailcall by deleting its entries from the BPF map. +func (t *Tracee) uninitTailCall(tailCall events.TailCall) error { + tailCallMapName := tailCall.GetMapName() + tailCallIndexes := tailCall.GetIndexes() + + // Pick eBPF map by name. + bpfMap, err := t.bpfModule.GetMap(tailCallMapName) + if err != nil { + return errfmt.WrapError(err) + } + + // Remove all indexes for this tailcall + for _, index := range tailCallIndexes { + // Workaround: Do not try to remove unsupported syscalls (arm64, e.g.) + if index >= uint32(events.Unsupported) { + continue + } + // Delete the entry from the BPF map + err := bpfMap.DeleteKey(unsafe.Pointer(&index)) + if err != nil { + // Log but don't fail on individual delete errors + logger.Debugw("Failed to uninit tailcall index", + "map", tailCallMapName, + "index", index, + "error", err) + } + } + + return nil +} + // initDerivationTable initializes tracee's events.DerivationTable. For each // event, represented through its ID, we declare to which other events it can be // derived and the corresponding function to derive into that Event. @@ -1302,25 +1333,6 @@ func (t *Tracee) populateBPFMaps() error { return errfmt.WrapError(err) } - // Initialize tail call dependencies - // TODO: Tail calls are not updated upon events changes in the dependency manager. - // Hence, upon events addition, fallbacks or removal, tail calls will not be updated. - // This should be fixed dynamically in the future. - for _, eventID := range t.eventsDependencies.GetEvents() { - depsNode, err := t.eventsDependencies.GetEvent(eventID) - if err != nil { - return errfmt.Errorf("failed to get event dependencies: %v", err) - } - deps := depsNode.GetDependencies() - tailCalls := deps.GetTailCalls() - for _, tailCall := range tailCalls { - err := t.initTailCall(tailCall) - if err != nil { - return errfmt.Errorf("failed to initialize tail call: %v", err) - } - } - } - return nil } @@ -1402,6 +1414,72 @@ func (t *Tracee) attachProbes() error { return nil } +// attachTailCalls initializes selected tailcalls by updating their BPF maps. +func (t *Tracee) attachTailCalls() error { + // Subscribe to watchers on the dependencies to initialize and/or uninitialize + // tailcalls upon changes + t.eventsDependencies.SubscribeAdd( + dependencies.TailCallNodeType, + func(node interface{}) []dependencies.Action { + tailCallNode, ok := node.(*dependencies.TailCallNode) + if !ok { + logger.Errorw("Got node from type not requested", "type", fmt.Sprintf("%T", node)) + return nil + } + tailCall := tailCallNode.GetTailCall() + err := t.initTailCall(tailCall) + if err != nil { + // Cancel adding this tailcall node if initialization fails + return []dependencies.Action{dependencies.NewCancelNodeAddAction(err)} + } + return nil + }) + + t.eventsDependencies.SubscribeRemove( + dependencies.TailCallNodeType, + func(node interface{}) []dependencies.Action { + tailCallNode, ok := node.(*dependencies.TailCallNode) + if !ok { + logger.Errorw("Got node from type not requested", "type", fmt.Sprintf("%T", node)) + return nil + } + tailCall := tailCallNode.GetTailCall() + err := t.uninitTailCall(tailCall) + if err != nil { + logger.Debugw("Failed to uninit tailcall", + "map", tailCall.GetMapName(), + "program", tailCall.GetProgName(), + "error", err) + } + return nil + }) + + // Initialize all current tailcalls or fail them (and dependent events) if initialization fails. + for _, tailCallKey := range t.eventsDependencies.GetTailCalls() { + tailCallNode, err := t.eventsDependencies.GetTailCall(tailCallKey) + if err != nil { + logger.Debugw("Failed to get tailcall from dependencies manager", "key", tailCallKey, "error", err) + continue + } + tailCall := tailCallNode.GetTailCall() + + // Initialize this specific tailcall + err = t.initTailCall(tailCall) + if err != nil { + // Mark this specific tailcall as failed, which will also fail all dependent events as needed. + failErr := t.eventsDependencies.FailTailCall(tailCall) + if failErr != nil { + logger.Warnw("Failed to fail tailcall in dependencies manager", + "tailcall", tailCallKey, + "init error", err, + "fail error", failErr) + } + } + } + + return nil +} + // validateProbesCompatibility validates the compatibility of probes and their fallbacks. // It will subscribe to the dependencies to validate the compatibility of new probes and their fallbacks as well. func (t *Tracee) validateProbesCompatibility() error { @@ -1579,6 +1657,12 @@ func (t *Tracee) initBPF() error { // iteration on procfs to fill in any missing data. err = t.attachProbes() + if err != nil { + return errfmt.WrapError(err) + } + + // Initialize tailcalls for selected events + err = t.attachTailCalls() return errfmt.WrapError(err) } diff --git a/pkg/events/definition_dependencies.go b/pkg/events/definition_dependencies.go index e6ff43537611..513e9f507955 100644 --- a/pkg/events/definition_dependencies.go +++ b/pkg/events/definition_dependencies.go @@ -217,3 +217,7 @@ func (tc TailCall) GetMapName() string { func (tc TailCall) GetProgName() string { return tc.progName } + +func (tc TailCall) IsRequired() bool { + return true // tailcalls are always required +} diff --git a/pkg/events/dependencies/event.go b/pkg/events/dependencies/event.go index b1baa5b54e12..3858f6f4a87a 100644 --- a/pkg/events/dependencies/event.go +++ b/pkg/events/dependencies/event.go @@ -60,7 +60,7 @@ func (en *EventNode) fallback() bool { } func (en *EventNode) GetDependents() []events.ID { - return slices.Clone[[]events.ID](en.dependents) + return slices.Clone(en.dependents) } func (en *EventNode) IsDependencyOf(dependent events.ID) bool { diff --git a/pkg/events/dependencies/manager.go b/pkg/events/dependencies/manager.go index e0626fd78b22..3c3ca0aa1d35 100644 --- a/pkg/events/dependencies/manager.go +++ b/pkg/events/dependencies/manager.go @@ -1,7 +1,6 @@ package dependencies import ( - "errors" "fmt" "maps" "reflect" @@ -16,10 +15,11 @@ import ( type NodeType string const ( - EventNodeType NodeType = "event" - ProbeNodeType NodeType = "probe" - AllNodeTypes NodeType = "all" - IllegalNodeType NodeType = "illegal" + EventNodeType NodeType = "event" + ProbeNodeType NodeType = "probe" + TailCallNodeType NodeType = "tailcall" + AllNodeTypes NodeType = "all" + IllegalNodeType NodeType = "illegal" ) // Manager is a management tree for the current dependencies of events. @@ -30,13 +30,16 @@ type Manager struct { mu sync.RWMutex events map[events.ID]*EventNode probes map[probes.Handle]*ProbeNode + tailCalls map[string]*TailCallNode // See .GetKey() method for the key format onAdd map[NodeType][]func(node interface{}) []Action onRemove map[NodeType][]func(node interface{}) []Action + onStateChanged []func() // Watchers called when manager state changes dependenciesGetter func(events.ID) events.DependencyStrategy // Track failed probes and events to prevent issues such as incorrect fallback handling, // duplicate processing, or inconsistent state when dependencies are shared between events. - failedProbes map[probes.Handle]struct{} - failedEvents map[events.ID]struct{} + failedProbes map[probes.Handle]struct{} + failedEvents map[events.ID]struct{} + failedTailCalls map[string]struct{} processingEvents map[events.ID]struct{} // Track events currently being processed - safeguard against recursive calls } @@ -46,11 +49,14 @@ func NewDependenciesManager(dependenciesGetter func(events.ID) events.Dependency mu: sync.RWMutex{}, events: make(map[events.ID]*EventNode), probes: make(map[probes.Handle]*ProbeNode), + tailCalls: make(map[string]*TailCallNode), onAdd: make(map[NodeType][]func(node interface{}) []Action), onRemove: make(map[NodeType][]func(node interface{}) []Action), + onStateChanged: make([]func(), 0), dependenciesGetter: dependenciesGetter, failedProbes: make(map[probes.Handle]struct{}), failedEvents: make(map[events.ID]struct{}), + failedTailCalls: make(map[string]struct{}), processingEvents: make(map[events.ID]struct{}), // Initialize processing state } } @@ -73,219 +79,15 @@ func (m *Manager) SubscribeRemove(subscribeType NodeType, onRemove func(node int m.onRemove[subscribeType] = append([]func(node interface{}) []Action{onRemove}, m.onRemove[subscribeType]...) } -// GetEvent returns the dependencies of the given event. -func (m *Manager) GetEvent(id events.ID) (*EventNode, error) { - m.mu.RLock() - defer m.mu.RUnlock() - - node := m.getEventNode(id) - if node == nil { - return nil, ErrNodeNotFound - } - return node, nil -} - -// GetProbe returns the given probe node managed by the Manager -func (m *Manager) GetProbe(handle probes.Handle) (*ProbeNode, error) { - m.mu.RLock() - defer m.mu.RUnlock() - - probeNode := m.getProbe(handle) - if probeNode == nil { - return nil, ErrNodeNotFound - } - return probeNode, nil -} - -// SelectEvent adds the given event to the management tree with default dependencies -// and marks it as explicitly selected. -// It also recursively adds all events that this event depends on (its dependencies) to the tree. -// This function has no effect if the event is already added. -func (m *Manager) SelectEvent(id events.ID) (*EventNode, error) { +// SubscribeStateChange adds a watcher function called when the manager's dependency tree changes. +// State change watchers are called in the order of their subscription. +// They are invoked only when nodes are actually added/removed from the tree or when fallbacks are applied. +// Changes to the "explicitly selected" status of existing nodes do not trigger these watchers. +func (m *Manager) SubscribeStateChange(onStateChanged func()) { m.mu.Lock() defer m.mu.Unlock() - return m.buildEvent(id, nil) -} - -// UnselectEvent marks the event as not explicitly selected. -// If the event is not a dependency of another event, it will be removed -// from the tree, and its dependencies will be cleaned if they are not referenced or explicitly selected. -// Returns whether it was removed. -func (m *Manager) UnselectEvent(id events.ID) bool { - m.mu.Lock() - defer m.mu.Unlock() - - node := m.getEventNode(id) - if node == nil { - return false - } - node.unmarkAsExplicitlySelected() - removed := m.cleanUnreferencedEventNode(node) - return removed -} - -// RemoveEvent removes the given event from the management tree. -// It removes its reference from its dependencies. If these events -// were added to the tree only as dependencies, they will be removed as well if -// they are not referenced by any other event anymore and not explicitly selected. -// It also removes all the events that depend on the given event (as their dependencies are -// no longer valid). -// It returns if managed to remove the event, as it might not be present in the tree. -func (m *Manager) RemoveEvent(id events.ID) error { - m.mu.Lock() - defer m.mu.Unlock() - - return m.removeEvent(id) -} - -// removeEvent removes the given event from the tree. -// It also fails all dependent events. -// removeEventNode should be used instead if you already have node reference to avoid search failures. -func (m *Manager) removeEvent(id events.ID) error { - node := m.getEventNode(id) - if node == nil { - return ErrNodeNotFound - } - return m.removeEventNode(node) -} - -// removeEventNode removes the given event node from the tree. -// It also fails all dependent events. -// This is the function to call with a node that might not be already registered in the tree. -// In general, it should be used if you already have node reference to avoid search failures. -func (m *Manager) removeEventNode(eventNode *EventNode) error { - eventName := events.Core.GetDefinitionByID(eventNode.GetID()).GetName() - logger.Debugw("Removing event from tree", "event", eventName) - - m.removeEventNodeFromDependencies(eventNode) - m.removeNode(eventNode) - m.failEventDependents(eventNode) - - return nil -} - -// buildEvent adds a new node for the given event if it does not exist in the tree. -// It is created with default dependencies. -// All dependencies nodes will also be created recursively with it. -// If the event exists in the tree, it will only update its dependents or its explicitlySelected -// value if it is built without dependents. -func (m *Manager) buildEvent(id events.ID, dependentEvents []events.ID) (*EventNode, error) { - // Resolve event name once for this function (only when needed for error logging) - eventName := events.Core.GetDefinitionByID(id).GetName() - - // Check if already processing this event (prevents loops) - if _, processing := m.processingEvents[id]; processing { - return nil, fmt.Errorf("event %s is currently being processed", eventName) - } - - if _, failed := m.failedEvents[id]; failed { - return nil, fmt.Errorf("event %s has previously failed", eventName) - } - - // Mark as processing - this is a safeguard against recursive calls so it is not important for it to be thread-safe with the check above. - m.processingEvents[id] = struct{}{} - defer delete(m.processingEvents, id) // Always cleanup - - explicitlySelected := len(dependentEvents) == 0 - node := m.getEventNode(id) - if node != nil { - if explicitlySelected { - node.markAsExplicitlySelected() - } - for _, dependent := range dependentEvents { - node.addDependent(dependent) - } - return node, nil - } - // Create node for the given ID and dependencies - dependencies := m.dependenciesGetter(id) - node = newDependenciesNode(id, dependencies, explicitlySelected) - for _, dependent := range dependentEvents { - node.addDependent(dependent) - } - _, err := m.buildEventNode(node) - if err != nil { - err = m.handleEventAddError(node, err) - if err != nil { - m.failedEvents[id] = struct{}{} - logger.Debugw("Event failed to build", "event", eventName, "error", err) - return nil, err - } - } - err = m.addNode(node) - if err != nil { - err = m.handleEventAddError(node, err) - if err != nil { - m.failedEvents[id] = struct{}{} - logger.Debugw("Event failed to add", "event", eventName, "error", err) - return nil, err - } - } - return node, nil -} - -// buildEventNode adds the dependencies of the current node to the tree and creates -// all needed references between nodes. -func (m *Manager) buildEventNode(eventNode *EventNode) (*EventNode, error) { - // Get the dependency event IDs - dependenciesIDs := eventNode.GetDependencies().GetIDs() - - // Build probe dependencies - for _, probe := range eventNode.GetDependencies().GetProbes() { - err := m.buildProbe(probe.GetHandle(), eventNode.GetID()) - if err != nil { - if probe.IsRequired() { - return nil, err - } - eventName := events.Core.GetDefinitionByID(eventNode.GetID()).GetName() - logger.Debugw( - "Non-required probe dependency adding failed for event", - "event", eventName, - "probe", probe.GetHandle(), "error", err, - ) - continue - } - } - - // Create nodes for all the events the node depends on and their dependencies recursively, - // or update them if they already exist - for _, dependencyID := range dependenciesIDs { - _, err := m.buildEvent( - dependencyID, - []events.ID{eventNode.GetID()}, - ) - if err != nil { - // If a dependency was cancelled, convert to failure for this dependent event - // This ensures dependent events use fallback mechanisms instead of being cancelled - var cancelErr *ErrNodeAddCancelled - if errors.As(err, &cancelErr) { - // Convert cancellation to failure for dependent events - failureReasons := make([]error, len(cancelErr.Reasons)) - for i, reason := range cancelErr.Reasons { - failureReasons[i] = fmt.Errorf("dependency cancelled: %w", reason) - } - return nil, NewErrNodeAddFailed(failureReasons) - } - return nil, err - } - } - return eventNode, nil -} - -func (m *Manager) getEventNode(id events.ID) *EventNode { - return m.events[id] -} - -// Nodes are added either because they are explicitly selected or because they are a dependency -// of another event. -func (m *Manager) addEventNode(eventNode *EventNode) { - m.events[eventNode.GetID()] = eventNode -} - -// removeEventNodeFromTree removes the node from the tree. -func (m *Manager) removeEventNodeFromTree(eventNode *EventNode) { - delete(m.events, eventNode.GetID()) + m.onStateChanged = append(m.onStateChanged, onStateChanged) } // addNode adds a node generically to the tree and triggers the on-add watchers. @@ -306,6 +108,8 @@ func (m *Manager) addNode(node interface{}) error { m.addEventNode(node.(*EventNode)) case ProbeNodeType: m.addProbeNodeToTree(node.(*ProbeNode)) + case TailCallNodeType: + m.addTailCallNodeToTree(node.(*TailCallNode)) } return nil } @@ -326,6 +130,8 @@ func (m *Manager) removeNode(node interface{}) { m.removeEventNodeFromTree(node.(*EventNode)) case ProbeNodeType: m.removeProbeNodeFromTree(node.(*ProbeNode)) + case TailCallNodeType: + m.removeTailCallNodeFromTree(node.(*TailCallNode)) } } @@ -398,239 +204,26 @@ func (m *Manager) triggerOnRemove(node interface{}) { } } +// triggerStateChanged triggers all state change watchers. +// This is called when the manager's state has actually changed. +func (m *Manager) triggerStateChanged() { + for _, onStateChanged := range m.onStateChanged { + onStateChanged() + } +} + func getNodeType(node interface{}) (NodeType, error) { switch node.(type) { case *EventNode: return EventNodeType, nil case *ProbeNode: return ProbeNodeType, nil + case *TailCallNode: + return TailCallNodeType, nil } return IllegalNodeType, fmt.Errorf("unknown node type: %s", reflect.TypeOf(node)) } -// cleanUnreferencedEventNode removes the node from the tree if it's not required anymore. -// It also removes all of its dependencies if they are not required anymore without it. -// Returns whether it was removed or not. -func (m *Manager) cleanUnreferencedEventNode(eventNode *EventNode) bool { - if len(eventNode.GetDependents()) > 0 || eventNode.isExplicitlySelected() { - return false - } - m.removeNode(eventNode) - m.removeEventNodeFromDependencies(eventNode) - return true -} - -// removeEventNodeFromDependencies removes the reference to the given node from its dependencies. -// It removes the dependencies from the tree if they are not chosen directly -// and no longer have any dependent event. -func (m *Manager) removeEventNodeFromDependencies(eventNode *EventNode) { - dependencyProbes := eventNode.GetDependencies().GetProbes() - for _, dependencyProbe := range dependencyProbes { - probe := m.getProbe(dependencyProbe.GetHandle()) - if probe == nil { - continue - } - probe.removeDependent(eventNode.GetID()) - if len(probe.GetDependents()) == 0 { - m.removeNode(probe) - } - } - - for _, dependencyEvent := range eventNode.GetDependencies().GetIDs() { - dependencyNode := m.getEventNode(dependencyEvent) - if dependencyNode == nil { - continue - } - dependencyNode.removeDependent(eventNode.GetID()) - m.cleanUnreferencedEventNode(dependencyNode) - } -} - -// failEventDependents fails all dependent events from the tree -func (m *Manager) failEventDependents(eventNode *EventNode) { - for _, dependentEvent := range eventNode.GetDependents() { - _, err := m.failEvent(dependentEvent) - if err != nil { - eventName := events.Core.GetDefinitionByID(dependentEvent).GetName() - logger.Debugw("failed to fail dependent event", "event", eventName, "error", err) - } - } -} - -func (m *Manager) getProbe(handle probes.Handle) *ProbeNode { - return m.probes[handle] -} - -// buildProbe adds the probe dependent to the probe node. -// It also creates the probe node if it does not exist in the tree. -func (m *Manager) buildProbe(handle probes.Handle, dependent events.ID) error { - if _, failed := m.failedProbes[handle]; failed { - return fmt.Errorf("probe %v previously failed to add", handle) - } - probeNode, ok := m.probes[handle] - if !ok { - probeNode = NewProbeNode(handle, []events.ID{dependent}) - err := m.addNode(probeNode) - if err != nil { - m.failedProbes[handle] = struct{}{} - return err - } - } else { - probeNode.addDependent(dependent) - } - return nil -} - -func (m *Manager) addProbeNodeToTree(probeNode *ProbeNode) { - m.probes[probeNode.GetHandle()] = probeNode -} - -// removeProbeNodeFromTree removes the node from the tree. -func (m *Manager) removeProbeNodeFromTree(handle *ProbeNode) { - delete(m.probes, handle.GetHandle()) -} - -// FailEvent is similar to RemoveEvent, except for the fact that instead of -// removing the current event immediately, it will try to use its fallback dependencies first. -// The old events dependencies of it will be removed in any case. -// The event will be removed if it has no fallback though, and with it the events -// that depend on it will fail as well. -// The return value specifies if the event was removed or not from the tree and any error that occurred. -func (m *Manager) FailEvent(id events.ID) (bool, error) { - m.mu.Lock() - defer m.mu.Unlock() - - return m.failEvent(id) -} - -// failEvent attempts to switch the given event dependencies to its next available fallback ones. -// It removes the event's current dependencies and tries each fallback in order. -// If a fallback is successfully applied (i.e., buildEventNode succeeds), the function returns (false, nil), -// indicating the event was not removed but switched to a fallback. -// If no fallbacks are available or all fail, the event is removed and the function returns (true, error), -// where the error is from the removal or nil if successful. -func (m *Manager) failEvent(eventID events.ID) (bool, error) { - // Check if already processing this event (prevents loops) - if _, processing := m.processingEvents[eventID]; processing { - return false, fmt.Errorf("event %v is currently being processed", eventID) - } - - node := m.getEventNode(eventID) - if node == nil { - return false, ErrNodeNotFound - } - - // Mark as processing - m.processingEvents[eventID] = struct{}{} - defer delete(m.processingEvents, eventID) // Always cleanup - - return m.failEventNode(node) -} - -func (m *Manager) failEventNode(node *EventNode) (bool, error) { - eventName := events.Core.GetDefinitionByID(node.GetID()).GetName() - logger.Debugw("Failing event", "event", eventName) - // Try fallbacks in a loop until one succeeds or we run out of fallbacks - for node.hasMoreFallbacks() { - m.removeEventNodeFromDependencies(node) - if !node.fallback() { - break - } - - _, err := m.buildEventNode(node) - if err == nil { - logger.Debugw("Successfully switched to fallback", "event", eventName, "fallback_index", node.currentFallbackIndex) - return false, nil - } - logger.Debugw("Failed to switch to fallback", "event", eventName, "fallback number", node.currentFallbackIndex, "error", err) - } - - logger.Debugw("All fallbacks failed, removing event", "event", eventName) - m.failedEvents[node.GetID()] = struct{}{} - return true, m.removeEventNode(node) -} - -// handleEventAddError handles the error of adding a node to the tree. -// As errors in adding a node to the tree are caused either by dependencies failure or by add-watchers actions, -// it will determine the handling strategy based on the error type. -func (m *Manager) handleEventAddError(node *EventNode, err error) error { - var cancelErr *ErrNodeAddCancelled - - if errors.As(err, &cancelErr) { - // Cancellation: immediate removal, no fallbacks - // No need to fail event dependents, as they will fail using event add failure error handling - eventName := events.Core.GetDefinitionByID(node.GetID()).GetName() - logger.Debugw("Event add cancelled", "event", eventName) - m.removeEventNodeFromDependencies(node) - m.removeNode(node) - return err - } - // Failure: try fallbacks - removed, failEventErr := m.failEventNode(node) - if failEventErr != nil { - logger.Errorw("Failed to fail", "error", failEventErr) - m.removeEventNodeFromDependencies(node) - return err - } - if removed { - // All fallbacks exhausted, event was removed - return err - } - // Fallback succeeded, no error to return - return nil -} - -// FailProbe marks a probe as failed and fails all events that depend on it. -func (m *Manager) FailProbe(handle probes.Handle) error { - m.mu.Lock() - defer m.mu.Unlock() - - // Mark probe as failed - m.failedProbes[handle] = struct{}{} - - m.removeProbe(handle) - - return nil -} - -// removeProbe removes the probe from the tree and fails all dependent events. -func (m *Manager) removeProbe(handle probes.Handle) { - probeNode := m.getProbe(handle) - if probeNode == nil { - return - } - - // Get all events that depend on this probe directly from the probe node - dependentEvents := probeNode.GetDependents() - - // Remove the probe node from the tree - m.removeNode(probeNode) - - // Fail all dependent events - for _, eventID := range dependentEvents { - // Check if the event is still in the tree and not being processed - if eventNode := m.getEventNode(eventID); eventNode != nil { - if _, processing := m.processingEvents[eventID]; !processing { - isRequired := true - for _, probe := range eventNode.GetDependencies().GetProbes() { - if probe.GetHandle() == handle { - isRequired = probe.IsRequired() - } - } - if !isRequired { - // We don't need to fail dependent events that don't have this probe as a required dependency - continue - } - _, err := m.failEvent(eventID) - if err != nil { - eventName := events.Core.GetDefinitionByID(eventID).GetName() - logger.Warnw("failed to fail dependent event", "event", eventName, "error", err) - } - } - } - } -} - func (m *Manager) GetEvents() []events.ID { m.mu.RLock() defer m.mu.RUnlock() @@ -642,3 +235,9 @@ func (m *Manager) GetProbes() []probes.Handle { defer m.mu.RUnlock() return slices.Collect(maps.Keys(m.probes)) } + +func (m *Manager) GetTailCalls() []string { + m.mu.RLock() + defer m.mu.RUnlock() + return slices.Collect(maps.Keys(m.tailCalls)) +} diff --git a/pkg/events/dependencies/manager_events.go b/pkg/events/dependencies/manager_events.go new file mode 100644 index 000000000000..c3fb7ce2808e --- /dev/null +++ b/pkg/events/dependencies/manager_events.go @@ -0,0 +1,412 @@ +package dependencies + +import ( + "errors" + "fmt" + + "github.com/aquasecurity/tracee/common/logger" + "github.com/aquasecurity/tracee/pkg/events" +) + +// GetEvent returns the dependencies of the given event. +func (m *Manager) GetEvent(id events.ID) (*EventNode, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + node := m.getEventNode(id) + if node == nil { + return nil, ErrNodeNotFound + } + return node, nil +} + +// SelectEvent adds the given event to the management tree with default dependencies +// and marks it as explicitly selected. +// It also recursively adds all events that this event depends on (its dependencies) to the tree. +// This function has no effect if the event is already added. +func (m *Manager) SelectEvent(id events.ID) (*EventNode, error) { + m.mu.Lock() + defer m.mu.Unlock() + + existingNode := m.getEventNode(id) + + node, err := m.buildEvent(id, nil) + if err != nil { + return node, err + } + + // Only trigger state change if this was a new node added to the tree + if existingNode == nil { + m.triggerStateChanged() + } + + return node, err +} + +// UnselectEvent marks the event as not explicitly selected. +// If the event is not a dependency of another event, it will be removed +// from the tree, and its dependencies will be cleaned if they are not referenced or explicitly selected. +// Returns whether it was removed. +func (m *Manager) UnselectEvent(id events.ID) bool { + m.mu.Lock() + defer m.mu.Unlock() + + node := m.getEventNode(id) + if node == nil { + return false + } + + node.unmarkAsExplicitlySelected() + removed := m.cleanUnreferencedEventNode(node) + + // Only trigger state change if the node was actually removed from the tree + if removed { + m.triggerStateChanged() + } + + return removed +} + +// RemoveEvent removes the given event from the management tree. +// It removes its reference from its dependencies. If these events +// were added to the tree only as dependencies, they will be removed as well if +// they are not referenced by any other event anymore and not explicitly selected. +// It also removes all the events that depend on the given event (as their dependencies are +// no longer valid). +// It returns if managed to remove the event, as it might not be present in the tree. +func (m *Manager) RemoveEvent(id events.ID) error { + m.mu.Lock() + defer m.mu.Unlock() + + err := m.removeEvent(id) + if err == nil { + m.triggerStateChanged() + } + + return err +} + +// removeEvent removes the given event from the tree. +// It also fails all dependent events. +// removeEventNode should be used instead if you already have node reference to avoid search failures. +func (m *Manager) removeEvent(id events.ID) error { + node := m.getEventNode(id) + if node == nil { + return ErrNodeNotFound + } + return m.removeEventNode(node) +} + +// removeEventNode removes the given event node from the tree. +// It also fails all dependent events. +// This is the function to call with a node that might not be already registered in the tree. +// In general, it should be used if you already have node reference to avoid search failures. +func (m *Manager) removeEventNode(eventNode *EventNode) error { + eventName := events.Core.GetDefinitionByID(eventNode.GetID()).GetName() + logger.Debugw("Removing event from tree", "event", eventName) + + m.removeEventNodeFromDependencies(eventNode) + m.removeNode(eventNode) + m.failEventDependents(eventNode) + + return nil +} + +// buildEvent adds a new node for the given event if it does not exist in the tree. +// It is created with default dependencies. +// All dependencies nodes will also be created recursively with it. +// If the event exists in the tree, it will only update its dependents or its explicitlySelected +// value if it is built without dependents. +func (m *Manager) buildEvent(id events.ID, dependentEvents []events.ID) (*EventNode, error) { + // Resolve event name once for this function (only when needed for error logging) + eventName := events.Core.GetDefinitionByID(id).GetName() + + // Check if already processing this event (prevents loops) + if _, processing := m.processingEvents[id]; processing { + return nil, fmt.Errorf("event %s is currently being processed", eventName) + } + + if _, failed := m.failedEvents[id]; failed { + return nil, fmt.Errorf("event %s has previously failed", eventName) + } + + // Mark as processing - this is a safeguard against recursive calls so it is not important for it to be thread-safe with the check above. + m.processingEvents[id] = struct{}{} + defer delete(m.processingEvents, id) // Always cleanup + + explicitlySelected := len(dependentEvents) == 0 + node := m.getEventNode(id) + if node != nil { + if explicitlySelected { + node.markAsExplicitlySelected() + } + for _, dependent := range dependentEvents { + node.addDependent(dependent) + } + return node, nil + } + // Create node for the given ID and dependencies + dependencies := m.dependenciesGetter(id) + node = newDependenciesNode(id, dependencies, explicitlySelected) + for _, dependent := range dependentEvents { + node.addDependent(dependent) + } + _, err := m.buildEventNode(node) + if err != nil { + err = m.handleEventAddError(node, err) + if err != nil { + m.failedEvents[id] = struct{}{} + logger.Debugw("Event failed to build", "event", eventName, "error", err) + return nil, err + } + } + err = m.addNode(node) + if err != nil { + err = m.handleEventAddError(node, err) + if err != nil { + m.failedEvents[id] = struct{}{} + logger.Debugw("Event failed to add", "event", eventName, "error", err) + return nil, err + } + } + return node, nil +} + +// buildEventNode adds the dependencies of the current node to the tree and creates +// all needed references between nodes. +func (m *Manager) buildEventNode(eventNode *EventNode) (*EventNode, error) { + // Get the dependency event IDs + dependenciesIDs := eventNode.GetDependencies().GetIDs() + + // Build probe dependencies + for _, probe := range eventNode.GetDependencies().GetProbes() { + err := m.buildProbe(probe.GetHandle(), eventNode.GetID()) + if err != nil { + if probe.IsRequired() { + return nil, err + } + eventName := events.Core.GetDefinitionByID(eventNode.GetID()).GetName() + logger.Debugw( + "Non-required probe dependency adding failed for event", + "event", eventName, + "probe", probe.GetHandle(), "error", err, + ) + continue + } + } + + // Build tailcall dependencies + for _, tailCall := range eventNode.GetDependencies().GetTailCalls() { + err := m.buildTailCall(tailCall, eventNode.GetID()) + if err != nil { + if tailCall.IsRequired() { + return nil, err + } + eventName := events.Core.GetDefinitionByID(eventNode.GetID()).GetName() + logger.Debugw( + "Non-required tailcall dependency adding failed for event", + "event", eventName, + "tailcall", GetTCKey(tailCall), "error", err, + ) + continue + } + } + + // Create nodes for all the events the node depends on and their dependencies recursively, + // or update them if they already exist + for _, dependencyID := range dependenciesIDs { + _, err := m.buildEvent( + dependencyID, + []events.ID{eventNode.GetID()}, + ) + if err != nil { + // If a dependency was cancelled, convert to failure for this dependent event + // This ensures dependent events use fallback mechanisms instead of being cancelled + var cancelErr *ErrNodeAddCancelled + if errors.As(err, &cancelErr) { + // Convert cancellation to failure for dependent events + failureReasons := make([]error, len(cancelErr.Reasons)) + for i, reason := range cancelErr.Reasons { + failureReasons[i] = fmt.Errorf("dependency cancelled: %w", reason) + } + return nil, NewErrNodeAddFailed(failureReasons) + } + return nil, err + } + } + return eventNode, nil +} + +func (m *Manager) getEventNode(id events.ID) *EventNode { + return m.events[id] +} + +// Nodes are added either because they are explicitly selected or because they are a dependency +// of another event. +func (m *Manager) addEventNode(eventNode *EventNode) { + m.events[eventNode.GetID()] = eventNode +} + +// removeEventNodeFromTree removes the node from the tree. +func (m *Manager) removeEventNodeFromTree(eventNode *EventNode) { + delete(m.events, eventNode.GetID()) +} + +// cleanUnreferencedEventNode removes the node from the tree if it's not required anymore. +// It also removes all of its dependencies if they are not required anymore without it. +// Returns whether it was removed or not. +func (m *Manager) cleanUnreferencedEventNode(eventNode *EventNode) bool { + if len(eventNode.GetDependents()) > 0 || eventNode.isExplicitlySelected() { + return false + } + m.removeNode(eventNode) + m.removeEventNodeFromDependencies(eventNode) + return true +} + +// removeEventNodeFromDependencies removes the reference to the given node from its dependencies. +// It removes the dependencies from the tree if they are not chosen directly +// and no longer have any dependent event. +func (m *Manager) removeEventNodeFromDependencies(eventNode *EventNode) { + dependencyProbes := eventNode.GetDependencies().GetProbes() + for _, dependencyProbe := range dependencyProbes { + probe := m.getProbe(dependencyProbe.GetHandle()) + if probe == nil { + continue + } + probe.removeDependent(eventNode.GetID()) + if len(probe.GetDependents()) == 0 { + m.removeNode(probe) + } + } + + dependencyTailCalls := eventNode.GetDependencies().GetTailCalls() + for _, dependencyTailCall := range dependencyTailCalls { + tailCall := m.getTailCall(GetTCKey(dependencyTailCall)) + if tailCall == nil { + continue + } + tailCall.removeDependent(eventNode.GetID()) + if len(tailCall.GetDependents()) == 0 { + m.removeNode(tailCall) + } + } + + for _, dependencyEvent := range eventNode.GetDependencies().GetIDs() { + dependencyNode := m.getEventNode(dependencyEvent) + if dependencyNode == nil { + continue + } + dependencyNode.removeDependent(eventNode.GetID()) + m.cleanUnreferencedEventNode(dependencyNode) + } +} + +// failEventDependents fails all dependent events from the tree +func (m *Manager) failEventDependents(eventNode *EventNode) { + for _, dependentEvent := range eventNode.GetDependents() { + _, err := m.failEvent(dependentEvent) + if err != nil { + eventName := events.Core.GetDefinitionByID(dependentEvent).GetName() + logger.Debugw("failed to fail dependent event", "event", eventName, "error", err) + } + } +} + +// FailEvent is similar to RemoveEvent, except for the fact that instead of +// removing the current event immediately, it will try to use its fallback dependencies first. +// The old events dependencies of it will be removed in any case. +// The event will be removed if it has no fallback though, and with it the events +// that depend on it will fail as well. +// The return value specifies if the event was removed or not from the tree and any error that occurred. +func (m *Manager) FailEvent(id events.ID) (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + + removed, err := m.failEvent(id) + // Always trigger state change on successful FailEvent since either: + // 1. Event was removed (state changed), or + // 2. Fallback was applied (dependencies changed, state changed) + if err == nil { + m.triggerStateChanged() + } + + return removed, err +} + +// failEvent attempts to switch the given event dependencies to its next available fallback ones. +// It removes the event's current dependencies and tries each fallback in order. +// If a fallback is successfully applied (i.e., buildEventNode succeeds), the function returns (false, nil), +// indicating the event was not removed but switched to a fallback. +// If no fallbacks are available or all fail, the event is removed and the function returns (true, error), +// where the error is from the removal or nil if successful. +func (m *Manager) failEvent(eventID events.ID) (bool, error) { + // Check if already processing this event (prevents loops) + if _, processing := m.processingEvents[eventID]; processing { + return false, fmt.Errorf("event %v is currently being processed", eventID) + } + + node := m.getEventNode(eventID) + if node == nil { + return false, ErrNodeNotFound + } + + // Mark as processing + m.processingEvents[eventID] = struct{}{} + defer delete(m.processingEvents, eventID) // Always cleanup + + return m.failEventNode(node) +} + +func (m *Manager) failEventNode(node *EventNode) (bool, error) { + eventName := events.Core.GetDefinitionByID(node.GetID()).GetName() + logger.Debugw("Failing event", "event", eventName) + // Try fallbacks in a loop until one succeeds or we run out of fallbacks + for node.hasMoreFallbacks() { + m.removeEventNodeFromDependencies(node) + if !node.fallback() { + break + } + + _, err := m.buildEventNode(node) + if err == nil { + logger.Debugw("Successfully switched to fallback", "event", eventName, "fallback_index", node.currentFallbackIndex) + return false, nil + } + logger.Debugw("Failed to switch to fallback", "event", eventName, "fallback number", node.currentFallbackIndex, "error", err) + } + + logger.Debugw("All fallbacks failed, removing event", "event", eventName) + m.failedEvents[node.GetID()] = struct{}{} + return true, m.removeEventNode(node) +} + +// handleEventAddError handles the error of adding a node to the tree. +// As errors in adding a node to the tree are caused either by dependencies failure or by add-watchers actions, +// it will determine the handling strategy based on the error type. +func (m *Manager) handleEventAddError(node *EventNode, err error) error { + var cancelErr *ErrNodeAddCancelled + + if errors.As(err, &cancelErr) { + // Cancellation: immediate removal, no fallbacks + // No need to fail event dependents, as they will fail using event add failure error handling + eventName := events.Core.GetDefinitionByID(node.GetID()).GetName() + logger.Debugw("Event add cancelled", "event", eventName) + m.removeEventNodeFromDependencies(node) + m.removeNode(node) + return err + } + // Failure: try fallbacks + removed, failEventErr := m.failEventNode(node) + if failEventErr != nil { + logger.Errorw("Failed to fail", "error", failEventErr) + m.removeEventNodeFromDependencies(node) + return err + } + if removed { + // All fallbacks exhausted, event was removed + return err + } + // Fallback succeeded, no error to return + return nil +} diff --git a/pkg/events/dependencies/manager_probes.go b/pkg/events/dependencies/manager_probes.go new file mode 100644 index 000000000000..6228dad610d7 --- /dev/null +++ b/pkg/events/dependencies/manager_probes.go @@ -0,0 +1,106 @@ +package dependencies + +import ( + "fmt" + + "github.com/aquasecurity/tracee/common/logger" + "github.com/aquasecurity/tracee/pkg/ebpf/probes" + "github.com/aquasecurity/tracee/pkg/events" +) + +// GetProbe returns the given probe node managed by the Manager +func (m *Manager) GetProbe(handle probes.Handle) (*ProbeNode, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + probeNode := m.getProbe(handle) + if probeNode == nil { + return nil, ErrNodeNotFound + } + return probeNode, nil +} + +func (m *Manager) getProbe(handle probes.Handle) *ProbeNode { + return m.probes[handle] +} + +// buildProbe adds the probe dependent to the probe node. +// It also creates the probe node if it does not exist in the tree. +func (m *Manager) buildProbe(handle probes.Handle, dependent events.ID) error { + if _, failed := m.failedProbes[handle]; failed { + return fmt.Errorf("probe %v previously failed to add", handle) + } + probeNode, ok := m.probes[handle] + if !ok { + probeNode = NewProbeNode(handle, []events.ID{dependent}) + err := m.addNode(probeNode) + if err != nil { + m.failedProbes[handle] = struct{}{} + return err + } + } else { + probeNode.addDependent(dependent) + } + return nil +} + +func (m *Manager) addProbeNodeToTree(probeNode *ProbeNode) { + m.probes[probeNode.GetHandle()] = probeNode +} + +// removeProbeNodeFromTree removes the node from the tree. +func (m *Manager) removeProbeNodeFromTree(handle *ProbeNode) { + delete(m.probes, handle.GetHandle()) +} + +// FailProbe marks a probe as failed and fails all events that depend on it. +func (m *Manager) FailProbe(handle probes.Handle) error { + m.mu.Lock() + defer m.mu.Unlock() + + // Mark probe as failed + m.failedProbes[handle] = struct{}{} + + m.removeProbe(handle) + m.triggerStateChanged() + + return nil +} + +// removeProbe removes the probe from the tree and fails all dependent events. +func (m *Manager) removeProbe(handle probes.Handle) { + probeNode := m.getProbe(handle) + if probeNode == nil { + return + } + + // Get all events that depend on this probe directly from the probe node + dependentEvents := probeNode.GetDependents() + + // Remove the probe node from the tree + m.removeNode(probeNode) + + // Fail all dependent events + for _, eventID := range dependentEvents { + // Check if the event is still in the tree and not being processed + if eventNode := m.getEventNode(eventID); eventNode != nil { + if _, processing := m.processingEvents[eventID]; !processing { + isRequired := true + for _, probe := range eventNode.GetDependencies().GetProbes() { + if probe.GetHandle() == handle { + isRequired = probe.IsRequired() + } + } + if !isRequired { + // We don't need to fail dependent events that don't have this probe as a required dependency + continue + } + _, err := m.failEvent(eventID) + if err != nil { + eventName := events.Core.GetDefinitionByID(eventID).GetName() + logger.Warnw("failed to fail dependent event", "event", eventName, "error", err) + } + } + } + } +} diff --git a/pkg/events/dependencies/manager_tailcalls.go b/pkg/events/dependencies/manager_tailcalls.go new file mode 100644 index 000000000000..56010e734129 --- /dev/null +++ b/pkg/events/dependencies/manager_tailcalls.go @@ -0,0 +1,119 @@ +package dependencies + +import ( + "fmt" + + "github.com/aquasecurity/tracee/common/logger" + "github.com/aquasecurity/tracee/pkg/events" +) + +// GetTailCall returns the given tailcall node managed by the Manager +func (m *Manager) GetTailCall(key string) (*TailCallNode, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + tailCallNode := m.getTailCall(key) + if tailCallNode == nil { + return nil, ErrNodeNotFound + } + return tailCallNode, nil +} + +func (m *Manager) getTailCall(key string) *TailCallNode { + return m.tailCalls[key] +} + +// buildTailCall adds the tailcall dependent to the tailcall node. +// It also creates the tailcall node if it does not exist in the tree. +func (m *Manager) buildTailCall(tailCall events.TailCall, dependent events.ID) error { + if _, failed := m.failedTailCalls[GetTCKey(tailCall)]; failed { + return fmt.Errorf("tailcall %v previously failed to add", GetTCKey(tailCall)) + } + tailCallNode, ok := m.tailCalls[GetTCKey(tailCall)] + if !ok { + tailCallNode = NewTailCallNode(tailCall, []events.ID{dependent}) + err := m.addNode(tailCallNode) + if err != nil { + m.failedTailCalls[GetTCKey(tailCall)] = struct{}{} + return err + } + } else { + // Merge indexes from the new tailcall into the existing node + // This handles the case where multiple events use the same map+program but different indexes + indexesMerged := tailCallNode.mergeIndexes(tailCall) + tailCallNode.addDependent(dependent) + + // If indexes were merged and the node was already in the tree, trigger state change + // so that subscribers (like BPF initialization) can re-process the updated tailcall + if indexesMerged { + logger.Debugw("Merged tailcall indexes", + "map", tailCall.GetMapName(), + "program", tailCall.GetProgName(), + "indexes", tailCallNode.GetTailCall().GetIndexes()) + // Trigger state changed to notify subscribers that tailcall indexes were updated + m.triggerStateChanged() + } + } + return nil +} + +func (m *Manager) addTailCallNodeToTree(tailCallNode *TailCallNode) { + m.tailCalls[GetTCKey(tailCallNode.GetTailCall())] = tailCallNode +} + +// removeTailCallNodeFromTree removes the node from the tree. +func (m *Manager) removeTailCallNodeFromTree(tailCallNode *TailCallNode) { + delete(m.tailCalls, GetTCKey(tailCallNode.GetTailCall())) +} + +// FailTailCall marks a tailcall as failed and fails all events that depend on it. +func (m *Manager) FailTailCall(tailCall events.TailCall) error { + m.mu.Lock() + defer m.mu.Unlock() + + // Mark tailcall as failed + m.failedTailCalls[GetTCKey(tailCall)] = struct{}{} + + m.removeTailCall(tailCall) + m.triggerStateChanged() + + return nil +} + +// removeTailCall removes the tailcall from the tree and fails all dependent events. +func (m *Manager) removeTailCall(tailCall events.TailCall) { + tailCallNode := m.getTailCall(GetTCKey(tailCall)) + if tailCallNode == nil { + return + } + + // Get all events that depend on this tailcall directly from the tailcall node + dependentEvents := tailCallNode.GetDependents() + + // Remove the tailcall node from the tree + m.removeNode(tailCallNode) + + // Fail all dependent events + for _, eventID := range dependentEvents { + // Check if the event is still in the tree and not being processed + if eventNode := m.getEventNode(eventID); eventNode != nil { + if _, processing := m.processingEvents[eventID]; !processing { + isRequired := true + for _, eventTC := range eventNode.GetDependencies().GetTailCalls() { + if GetTCKey(eventTC) == GetTCKey(tailCall) { + isRequired = eventTC.IsRequired() + } + } + if !isRequired { + // We don't need to fail dependent events that don't have this tailcall as a required dependency + continue + } + _, err := m.failEvent(eventID) + if err != nil { + eventName := events.Core.GetDefinitionByID(eventID).GetName() + logger.Warnw("failed to fail dependent event", "event", eventName, "error", err) + } + } + } + } +} diff --git a/pkg/events/dependencies/tailcalls.go b/pkg/events/dependencies/tailcalls.go new file mode 100644 index 000000000000..a564d6e35913 --- /dev/null +++ b/pkg/events/dependencies/tailcalls.go @@ -0,0 +1,61 @@ +package dependencies + +import ( + "slices" + + "github.com/aquasecurity/tracee/pkg/events" +) + +// TailCallNode represents a tailcall entry in the dependency tree. +// A tailcall is identified by its map name, program name, and index. +type TailCallNode struct { + tailCall events.TailCall + dependents []events.ID // Events that depend on this tailcall +} + +func NewTailCallNode(tailCall events.TailCall, dependents []events.ID) *TailCallNode { + return &TailCallNode{ + tailCall: tailCall, + dependents: dependents, + } +} + +func (tn *TailCallNode) GetTailCall() events.TailCall { + return tn.tailCall +} + +// mergeIndexes merges new indexes into the existing tailcall. +// This is needed when multiple events use the same map+program combination but with different indexes. +// Returns true if indexes were actually merged (changed), false if no change occurred. +func (tn *TailCallNode) mergeIndexes(newTailCall events.TailCall) bool { + oldIndexCount := len(tn.tailCall.GetIndexes()) + // Use the helper function from events package to create a new TailCall with merged indexes + tn.tailCall = events.NewTailCallWithMergedIndexes(tn.tailCall, newTailCall.GetIndexes()) + newIndexCount := len(tn.tailCall.GetIndexes()) + // Return true if the number of indexes changed (indicating a merge occurred) + return newIndexCount != oldIndexCount +} + +func (tn *TailCallNode) GetDependents() []events.ID { + return slices.Clone(tn.dependents) +} + +func (tn *TailCallNode) addDependent(dependent events.ID) { + tn.dependents = append(tn.dependents, dependent) +} + +func (tn *TailCallNode) removeDependent(dependent events.ID) { + for i, d := range tn.dependents { + if d == dependent { + tn.dependents = append(tn.dependents[:i], tn.dependents[i+1:]...) + break + } + } +} + +// GetTCKey returns a unique identifier for this tailcall based on map name and program name. +// Multiple events can share the same map+program combination with different indexes, +// so indexes are merged within the TailCallNode rather than being part of the key. +func GetTCKey(tailCall events.TailCall) string { + return tailCall.GetMapName() + ":" + tailCall.GetProgName() +} diff --git a/tests/integration/event_filters_test.go b/tests/integration/event_filters_test.go index af6317bce271..c5cc567fc870 100644 --- a/tests/integration/event_filters_test.go +++ b/tests/integration/event_filters_test.go @@ -15,6 +15,7 @@ import ( "go.uber.org/goleak" "github.com/aquasecurity/tracee/common/bitwise" + "github.com/aquasecurity/tracee/common/logger" "github.com/aquasecurity/tracee/pkg/config" "github.com/aquasecurity/tracee/pkg/events" k8s "github.com/aquasecurity/tracee/pkg/k8s/apis/tracee.aquasec.com/v1beta1" @@ -32,6 +33,10 @@ func Test_EventFilters(t *testing.T) { // If a test case fails, ignore the leak since it's probably caused by the aborted test. defer goleak.VerifyNone(t) + // Setup test logger + teardown := testutils.EnableTestLogger(t, logger.InfoLevel) + defer teardown() + // test table tt := []testCase{ // events matched in single policies - detached workloads