From 02ba92fc55cf67095a63d09a4f85231411db1917 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Mon, 12 Jan 2026 12:14:31 +0200 Subject: [PATCH 01/17] rule engine redesign --- cmd/main.go | 18 +++- pkg/contextdetection/detectors/host.go | 60 +++++++++++++ pkg/contextdetection/detectors/k8s.go | 54 ++++++++++++ pkg/contextdetection/detectors/manager.go | 70 +++++++++++++++ pkg/contextdetection/detectors/standalone.go | 56 ++++++++++++ pkg/contextdetection/hostutil.go | 21 +++++ pkg/contextdetection/registry.go | 92 ++++++++++++++++++++ pkg/contextdetection/types.go | 23 +++++ pkg/ebpf/events/enriched_event.go | 21 +++-- pkg/rulemanager/containercallbacks.go | 19 ++++ pkg/rulemanager/rule_manager.go | 76 ++++++++++++++-- 11 files changed, 495 insertions(+), 15 deletions(-) create mode 100644 pkg/contextdetection/detectors/host.go create mode 100644 pkg/contextdetection/detectors/k8s.go create mode 100644 pkg/contextdetection/detectors/manager.go create mode 100644 pkg/contextdetection/detectors/standalone.go create mode 100644 pkg/contextdetection/hostutil.go create mode 100644 pkg/contextdetection/registry.go create mode 100644 pkg/contextdetection/types.go diff --git a/cmd/main.go b/cmd/main.go index 0545fda174..c6e39be302 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -29,6 +29,7 @@ import ( containerprofilemanagerv1 "github.com/kubescape/node-agent/pkg/containerprofilemanager/v1" "github.com/kubescape/node-agent/pkg/containerwatcher" containerwatcherv2 "github.com/kubescape/node-agent/pkg/containerwatcher/v2" + "github.com/kubescape/node-agent/pkg/contextdetection" "github.com/kubescape/node-agent/pkg/dnsmanager" "github.com/kubescape/node-agent/pkg/exporters" "github.com/kubescape/node-agent/pkg/fimmanager" @@ -290,8 +291,23 @@ func main() { logger.L().Ctx(ctx).Fatal("error creating CEL evaluator", helpers.Error(err)) } + // Create and initialize MntnsRegistry for host context monitoring + mntnsRegistry := contextdetection.NewMntnsRegistry() + + // Auto-detect and initialize host mount namespace + hostMntns, err := contextdetection.GetCurrentHostMntns() + if err != nil { + logger.L().Ctx(ctx).Warning("failed to detect host mount namespace", + helpers.Error(err)) + } else { + if err := mntnsRegistry.SetHostMntns(hostMntns); err != nil { + logger.L().Ctx(ctx).Warning("failed to set host mount namespace", + helpers.Error(err)) + } + } + // create runtimeDetection managers - ruleManager, err = rulemanager.CreateRuleManager(ctx, cfg, k8sClient, ruleBindingCache, objCache, exporter, prometheusExporter, processTreeManager, dnsResolver, nil, ruleCooldown, adapterFactory, celEvaluator) + ruleManager, err = rulemanager.CreateRuleManager(ctx, cfg, k8sClient, ruleBindingCache, objCache, exporter, prometheusExporter, processTreeManager, dnsResolver, nil, ruleCooldown, adapterFactory, celEvaluator, mntnsRegistry) if err != nil { logger.L().Ctx(ctx).Fatal("error creating RuleManager", helpers.Error(err)) } diff --git a/pkg/contextdetection/detectors/host.go b/pkg/contextdetection/detectors/host.go new file mode 100644 index 0000000000..5abdc8808d --- /dev/null +++ b/pkg/contextdetection/detectors/host.go @@ -0,0 +1,60 @@ +package detectors + +import ( + "os" + + containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" + "github.com/kubescape/node-agent/pkg/contextdetection" +) + +type HostContextInfo struct { + HostName string +} + +func (h *HostContextInfo) Context() contextdetection.EventSourceContext { + return contextdetection.Host +} + +// WorkloadID returns the hostname as the workload identifier for host contexts. +// Returns the system hostname if available, or "host" as a fallback. +// This identifier is unique and deterministic within host contexts. +func (h *HostContextInfo) WorkloadID() string { + if h.HostName != "" { + return h.HostName + } + return "host" +} + +type HostDetector struct { + name string + hostName string +} + +func NewHostDetector() *HostDetector { + hostName, _ := os.Hostname() + return &HostDetector{ + name: "HostDetector", + hostName: hostName, + } +} + +func (hd *HostDetector) Detect(container *containercollection.Container) (contextdetection.ContextInfo, bool) { + if container == nil { + return nil, false + } + + if container.K8s.PodName != "" || container.K8s.Namespace != "" { + return nil, false + } + + if container.Runtime.ContainerID != "" { + hostInfo := &HostContextInfo{HostName: hd.hostName} + return hostInfo, true + } + + return nil, false +} + +func (hd *HostDetector) Priority() int { + return 1 +} diff --git a/pkg/contextdetection/detectors/k8s.go b/pkg/contextdetection/detectors/k8s.go new file mode 100644 index 0000000000..2a263a3b5b --- /dev/null +++ b/pkg/contextdetection/detectors/k8s.go @@ -0,0 +1,54 @@ +package detectors + +import ( + "fmt" + + containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" + "github.com/kubescape/node-agent/pkg/contextdetection" +) + +type K8sContextInfo struct { + Namespace string + PodName string + Workload string + WorkloadUID string +} + +func (k *K8sContextInfo) Context() contextdetection.EventSourceContext { + return contextdetection.Kubernetes +} + +// WorkloadID returns the Kubernetes workload identifier in the format "namespace/podname". +// This format is standardized and deterministic within Kubernetes contexts. +func (k *K8sContextInfo) WorkloadID() string { + return fmt.Sprintf("%s/%s", k.Namespace, k.PodName) +} + +type K8sDetector struct { + name string +} + +func NewK8sDetector() *K8sDetector { + return &K8sDetector{name: "K8sDetector"} +} + +func (kd *K8sDetector) Detect(container *containercollection.Container) (contextdetection.ContextInfo, bool) { + if container == nil { + return nil, false + } + + if container.K8s.PodName == "" || container.K8s.Namespace == "" { + return nil, false + } + + k8sInfo := &K8sContextInfo{ + Namespace: container.K8s.Namespace, + PodName: container.K8s.PodName, + } + + return k8sInfo, true +} + +func (kd *K8sDetector) Priority() int { + return 0 +} diff --git a/pkg/contextdetection/detectors/manager.go b/pkg/contextdetection/detectors/manager.go new file mode 100644 index 0000000000..3eea697acf --- /dev/null +++ b/pkg/contextdetection/detectors/manager.go @@ -0,0 +1,70 @@ +package detectors + +import ( + "errors" + + containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" + "github.com/kubescape/node-agent/pkg/contextdetection" +) + +// DetectorManager orchestrates context detection across multiple detectors. +// It executes detectors in priority order and returns the first match. +type DetectorManager struct { + registry *contextdetection.MntnsRegistry + detectors []contextdetection.ContextDetector +} + +// NewDetectorManager creates a new DetectorManager with the given registry. +// Detectors are initialized in priority order: K8s (0), Host (1), Standalone (2). +func NewDetectorManager(registry *contextdetection.MntnsRegistry) *DetectorManager { + dm := &DetectorManager{ + registry: registry, + detectors: []contextdetection.ContextDetector{ + NewK8sDetector(), + NewHostDetector(), + NewStandaloneDetector(), + }, + } + + return dm +} + +// DetectContext detects the context of a container by running detectors in priority order. +// Returns the ContextInfo from the first detector that matches, or a default Standalone context +// if no detector matches. +func (dm *DetectorManager) DetectContext(container *containercollection.Container) (contextdetection.ContextInfo, error) { + if container == nil { + logger.L().Warning("DetectorManager - nil container provided") + return nil, ErrInvalidContainer + } + + containerID := container.Runtime.ContainerID + logger.L().Debug("DetectorManager - detecting context for container", + helpers.String("containerID", containerID)) + + for _, detector := range dm.detectors { + contextInfo, detected := detector.Detect(container) + if detected { + logger.L().Debug("DetectorManager - detected context", + helpers.String("containerID", containerID), + helpers.String("context", string(contextInfo.Context())), + helpers.String("workloadID", contextInfo.WorkloadID())) + return contextInfo, nil + } + } + + logger.L().Warning("DetectorManager - no detector matched, defaulting to Standalone", + helpers.String("containerID", containerID)) + + return &StandaloneContextInfo{ + ContainerID: container.Runtime.ContainerID, + ContainerName: container.Runtime.ContainerName, + }, nil +} + +var ( + // ErrInvalidContainer is returned when a nil container is provided to DetectContext + ErrInvalidContainer = errors.New("invalid container: nil provided") +) diff --git a/pkg/contextdetection/detectors/standalone.go b/pkg/contextdetection/detectors/standalone.go new file mode 100644 index 0000000000..67bbaa31b3 --- /dev/null +++ b/pkg/contextdetection/detectors/standalone.go @@ -0,0 +1,56 @@ +package detectors + +import ( + containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" + "github.com/kubescape/node-agent/pkg/contextdetection" +) + +type StandaloneContextInfo struct { + ContainerID string + ContainerName string +} + +func (s *StandaloneContextInfo) Context() contextdetection.EventSourceContext { + return contextdetection.Standalone +} + +// WorkloadID returns a unique identifier for standalone containers. +// Format priority: +// 1. "name:containerID" if both name and ID are available +// 2. containerName if ID is empty +// 3. containerID if name is empty +// This identifier is deterministic and unique within standalone contexts. +func (s *StandaloneContextInfo) WorkloadID() string { + if s.ContainerID == "" { + return s.ContainerName + } + if s.ContainerName != "" { + return s.ContainerName + ":" + s.ContainerID + } + return s.ContainerID +} + +type StandaloneDetector struct { + name string +} + +func NewStandaloneDetector() *StandaloneDetector { + return &StandaloneDetector{name: "StandaloneDetector"} +} + +func (sd *StandaloneDetector) Detect(container *containercollection.Container) (contextdetection.ContextInfo, bool) { + if container == nil { + return nil, false + } + + standaloneInfo := &StandaloneContextInfo{ + ContainerID: container.Runtime.ContainerID, + ContainerName: container.Runtime.ContainerName, + } + + return standaloneInfo, true +} + +func (sd *StandaloneDetector) Priority() int { + return 2 +} diff --git a/pkg/contextdetection/hostutil.go b/pkg/contextdetection/hostutil.go new file mode 100644 index 0000000000..fa692d6b21 --- /dev/null +++ b/pkg/contextdetection/hostutil.go @@ -0,0 +1,21 @@ +package contextdetection + +import ( + "fmt" + + containerutils "github.com/inspektor-gadget/inspektor-gadget/pkg/container-utils" + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" +) + +// GetCurrentHostMntns reads the host's mount namespace ID from PID 1 (init process). +func GetCurrentHostMntns() (uint64, error) { + mntns, err := containerutils.GetMntNs(1) + if err != nil { + logger.L().Error("failed to detect host mount namespace from PID 1", + helpers.Error(err)) + return 0, fmt.Errorf("failed to get host mount namespace: %w", err) + } + + return mntns, nil +} diff --git a/pkg/contextdetection/registry.go b/pkg/contextdetection/registry.go new file mode 100644 index 0000000000..90953ab24b --- /dev/null +++ b/pkg/contextdetection/registry.go @@ -0,0 +1,92 @@ +package contextdetection + +import ( + "errors" + "fmt" + + "github.com/goradd/maps" + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" +) + +var ( + ErrInvalidMntns = errors.New("invalid mount namespace ID: cannot be zero") +) + +// MntnsRegistry maps mount namespace IDs to their context information. +// It uses SafeMap for thread-safe concurrent access. +type MntnsRegistry struct { + entries maps.SafeMap[uint64, ContextInfo] + hostMntns uint64 +} + +// NewMntnsRegistry creates a new MntnsRegistry instance. +// The hostMntns should be set separately via SetHostMntns after initialization. +func NewMntnsRegistry() *MntnsRegistry { + return &MntnsRegistry{} +} + +// Register adds or updates a mount namespace entry in the registry. +func (r *MntnsRegistry) Register(mntns uint64, info ContextInfo) error { + if mntns == 0 { + return ErrInvalidMntns + } + + if r.entries.Has(mntns) { + logger.L().Warning("MntnsRegistry - mount namespace already registered, skipping", + helpers.String("mntns", fmt.Sprintf("%d", mntns)), + helpers.String("context", string(info.Context()))) + return nil + } + + r.entries.Set(mntns, info) + logger.L().Debug("MntnsRegistry - registered mount namespace", + helpers.String("mntns", fmt.Sprintf("%d", mntns)), + helpers.String("context", string(info.Context())), + helpers.String("workloadID", info.WorkloadID())) + + return nil +} + +// Lookup retrieves the context information for a mount namespace. +func (r *MntnsRegistry) Lookup(mntns uint64) (ContextInfo, bool) { + info, ok := r.entries.Load(mntns) + return info, ok +} + +// Unregister removes a mount namespace entry from the registry. +func (r *MntnsRegistry) Unregister(mntns uint64) { + if r.entries.Has(mntns) { + r.entries.Delete(mntns) + logger.L().Debug("MntnsRegistry - unregistered mount namespace", + helpers.String("mntns", fmt.Sprintf("%d", mntns))) + } +} + +// SetHostMntns sets the host's mount namespace ID. +func (r *MntnsRegistry) SetHostMntns(mntns uint64) error { + if mntns == 0 { + return ErrInvalidMntns + } + + r.hostMntns = mntns + logger.L().Info("MntnsRegistry - host mount namespace set", + helpers.String("mntns", fmt.Sprintf("%d", mntns))) + + return nil +} + +// GetHostMntns returns the host's mount namespace ID. +func (r *MntnsRegistry) GetHostMntns() uint64 { + return r.hostMntns +} + +// IsHostMntns checks if the given mount namespace ID is the host's. +func (r *MntnsRegistry) IsHostMntns(mntns uint64) bool { + return r.hostMntns != 0 && mntns == r.hostMntns +} + +// Size returns the number of entries in the registry. +func (r *MntnsRegistry) Size() int { + return r.entries.Len() +} diff --git a/pkg/contextdetection/types.go b/pkg/contextdetection/types.go new file mode 100644 index 0000000000..c6889a8cb2 --- /dev/null +++ b/pkg/contextdetection/types.go @@ -0,0 +1,23 @@ +package contextdetection + +import ( + containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" +) + +type EventSourceContext string + +const ( + Kubernetes EventSourceContext = "kubernetes" + Host EventSourceContext = "host" + Standalone EventSourceContext = "standalone" +) + +type ContextInfo interface { + Context() EventSourceContext + WorkloadID() string +} + +type ContextDetector interface { + Detect(container *containercollection.Container) (ContextInfo, bool) + Priority() int +} diff --git a/pkg/ebpf/events/enriched_event.go b/pkg/ebpf/events/enriched_event.go index 5f04a8476b..9d869ef597 100644 --- a/pkg/ebpf/events/enriched_event.go +++ b/pkg/ebpf/events/enriched_event.go @@ -4,6 +4,7 @@ import ( "time" apitypes "github.com/armosec/armoapi-go/armotypes" + "github.com/kubescape/node-agent/pkg/contextdetection" "github.com/kubescape/node-agent/pkg/utils" ) @@ -18,10 +19,18 @@ func NewEnrichedEvent(event utils.K8sEvent, timestamp time.Time, containerID str } type EnrichedEvent struct { - Event utils.K8sEvent - Timestamp time.Time - ContainerID string - ProcessTree apitypes.Process - PID uint32 - PPID uint32 + Event utils.K8sEvent + Timestamp time.Time + ContainerID string + ProcessTree apitypes.Process + PID uint32 + PPID uint32 + // SourceContext holds the context information for this event (K8s, Host, or Standalone). + // This is populated during event enrichment if the feature is enabled. + // May be nil for legacy K8s-only events or when feature is disabled. + SourceContext contextdetection.ContextInfo + // MountNamespaceID is the mount namespace ID from the container. + // This uniquely identifies the container/host and is used for context lookup. + // May be 0 if unavailable. + MountNamespaceID uint64 } diff --git a/pkg/rulemanager/containercallbacks.go b/pkg/rulemanager/containercallbacks.go index d248dac1a3..f9415a4596 100644 --- a/pkg/rulemanager/containercallbacks.go +++ b/pkg/rulemanager/containercallbacks.go @@ -62,6 +62,20 @@ func (rm *RuleManager) ContainerCallback(notif containercollection.PubSubEvent) return } + // Detect context and register in the registry + contextInfo, err := rm.detectorManager.DetectContext(notif.Container) + if err != nil { + logger.L().Warning("RuleManager - failed to detect context", + helpers.String("container ID", notif.Container.Runtime.ContainerID), + helpers.Error(err)) + } else if contextInfo != nil && notif.Container.Mntns != 0 { + if err := rm.mntnsRegistry.Register(notif.Container.Mntns, contextInfo); err != nil { + logger.L().Warning("RuleManager - failed to register in mntns registry", + helpers.String("container ID", notif.Container.Runtime.ContainerID), + helpers.Error(err)) + } + } + rm.trackedContainers.Add(k8sContainerID) shim, err := utils.GetProcessStat(int(notif.Container.ContainerPid())) if err != nil { @@ -76,6 +90,11 @@ func (rm *RuleManager) ContainerCallback(notif containercollection.PubSubEvent) helpers.String("container ID", notif.Container.Runtime.ContainerID), helpers.String("k8s workload", k8sContainerID)) + // Unregister from the context registry + if notif.Container.Mntns != 0 { + rm.mntnsRegistry.Unregister(notif.Container.Mntns) + } + rm.trackedContainers.Remove(k8sContainerID) namespace := notif.Container.K8s.Namespace podName := notif.Container.K8s.PodName diff --git a/pkg/rulemanager/rule_manager.go b/pkg/rulemanager/rule_manager.go index f39b072317..6bff21e692 100644 --- a/pkg/rulemanager/rule_manager.go +++ b/pkg/rulemanager/rule_manager.go @@ -14,6 +14,8 @@ import ( "github.com/kubescape/go-logger/helpers" helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" "github.com/kubescape/node-agent/pkg/config" + "github.com/kubescape/node-agent/pkg/contextdetection" + "github.com/kubescape/node-agent/pkg/contextdetection/detectors" "github.com/kubescape/node-agent/pkg/dnsmanager" "github.com/kubescape/node-agent/pkg/ebpf/events" "github.com/kubescape/node-agent/pkg/exporters" @@ -57,6 +59,8 @@ type RuleManager struct { adapterFactory *ruleadapters.EventRuleAdapterFactory ruleFailureCreator ruleadapters.RuleFailureCreatorInterface rulePolicyValidator *RulePolicyValidator + mntnsRegistry *contextdetection.MntnsRegistry + detectorManager *detectors.DetectorManager } var _ RuleManagerClient = (*RuleManager)(nil) @@ -75,10 +79,14 @@ func CreateRuleManager( ruleCooldown *rulecooldown.RuleCooldown, adapterFactory *ruleadapters.EventRuleAdapterFactory, celEvaluator cel.CELRuleEvaluator, + mntnsRegistry *contextdetection.MntnsRegistry, ) (*RuleManager, error) { ruleFailureCreator := ruleadapters.NewRuleFailureCreator(enricher, dnsManager, adapterFactory) rulePolicyValidator := NewRulePolicyValidator(objectCache) + // Create a detector manager for context detection + detectorManager := detectors.NewDetectorManager(mntnsRegistry) + r := &RuleManager{ cfg: cfg, ctx: ctx, @@ -95,6 +103,8 @@ func CreateRuleManager( celEvaluator: celEvaluator, ruleFailureCreator: ruleFailureCreator, rulePolicyValidator: rulePolicyValidator, + mntnsRegistry: mntnsRegistry, + detectorManager: detectorManager, } return r, nil @@ -125,20 +135,49 @@ func (rm *RuleManager) startRuleManager(container *containercollection.Container } func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) { + // Enrich event with source context and mount namespace ID for context-aware rule evaluation + rm.enrichEventWithContext(enrichedEvent) + var profileExists bool + var details string namespace := enrichedEvent.Event.GetNamespace() pod := enrichedEvent.Event.GetPod() - podId := utils.CreateK8sPodID(namespace, pod) - details, ok := rm.podToWlid.Load(podId) - if !ok { - return + + // Determine workload ID based on context type + isK8sContext := enrichedEvent.SourceContext == nil || enrichedEvent.SourceContext.Context() == contextdetection.Kubernetes + + if isK8sContext { + // K8s context: use pod-based workload ID from cache + if pod == "" || namespace == "" { + return + } + podId := utils.CreateK8sPodID(namespace, pod) + var ok bool + details, ok = rm.podToWlid.Load(podId) + if !ok { + return + } + } else { + // Host or Standalone context: use SourceContext.WorkloadID() + if enrichedEvent.SourceContext == nil { + return + } + details = enrichedEvent.SourceContext.WorkloadID() + if details == "" { + return + } } - if pod == "" || namespace == "" { - return + // Retrieve rules based on context: K8s uses pod-based bindings, non-K8s uses all rules + var rules []typesv1.Rule + if enrichedEvent.SourceContext == nil || enrichedEvent.SourceContext.Context() == contextdetection.Kubernetes { + // K8s context: use pod-based rule bindings + rules = rm.ruleBindingCache.ListRulesForPod(namespace, pod) + } else { + // Host or Standalone context: retrieve all rules (rule filtering deferred to future phases) + rules = rm.ruleBindingCache.GetRuleCreator().CreateAllRules() } - rules := rm.ruleBindingCache.ListRulesForPod(namespace, pod) if len(rules) == 0 { return } @@ -155,7 +194,9 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) if !rule.Enabled { continue } - if !profileExists && rule.ProfileDependency == armotypes.Required { + // Skip profile dependency checks for non-K8s contexts (profiles are K8s-specific) + // Only K8s contexts should enforce profile dependencies + if isK8sContext && !profileExists && rule.ProfileDependency == armotypes.Required { continue } @@ -211,6 +252,25 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) } } +func (rm *RuleManager) enrichEventWithContext(enrichedEvent *events.EnrichedEvent) { + // Extract mount namespace ID from the event + mntnsID := uint64(0) + if enrichEvent, ok := enrichedEvent.Event.(utils.EnrichEvent); ok { + mntnsID = enrichEvent.GetMountNsID() + } + enrichedEvent.MountNamespaceID = mntnsID + + // Look up context information for this mount namespace + if mntnsID > 0 { + if contextInfo, found := rm.mntnsRegistry.Lookup(mntnsID); found { + enrichedEvent.SourceContext = contextInfo + logger.L().Debug("RuleManager - enriched event with context", + helpers.String("mntns", fmt.Sprintf("%d", mntnsID)), + helpers.String("context", string(contextInfo.Context()))) + } + } +} + func (rm *RuleManager) HasApplicableRuleBindings(namespace, name string) bool { return len(rm.ruleBindingCache.ListRulesForPod(namespace, name)) > 0 } From 90ddcd0984bfd3b6aadf4741128f7ab26fde4543 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Tue, 13 Jan 2026 14:29:48 +0200 Subject: [PATCH 02/17] support both ExecutionContexts field and context tags --- pkg/rulemanager/rule_manager.go | 49 ++++++++++++++++++++----------- pkg/rulemanager/rulepolicy.go | 37 +++++++++++++++++++++++ pkg/rulemanager/types/v1/types.go | 1 + pkg/utils/datasource_event.go | 13 ++++++-- 4 files changed, 81 insertions(+), 19 deletions(-) diff --git a/pkg/rulemanager/rule_manager.go b/pkg/rulemanager/rule_manager.go index 6bff21e692..c0092dd51d 100644 --- a/pkg/rulemanager/rule_manager.go +++ b/pkg/rulemanager/rule_manager.go @@ -88,23 +88,23 @@ func CreateRuleManager( detectorManager := detectors.NewDetectorManager(mntnsRegistry) r := &RuleManager{ - cfg: cfg, - ctx: ctx, - k8sClient: k8sClient, - trackedContainers: mapset.NewSet[string](), - ruleBindingCache: ruleBindingCache, - objectCache: objectCache, - exporter: exporter, - metrics: metrics, - adapterFactory: adapterFactory, - enricher: enricher, - processManager: processManager, - ruleCooldown: ruleCooldown, - celEvaluator: celEvaluator, - ruleFailureCreator: ruleFailureCreator, - rulePolicyValidator: rulePolicyValidator, - mntnsRegistry: mntnsRegistry, - detectorManager: detectorManager, + cfg: cfg, + ctx: ctx, + k8sClient: k8sClient, + trackedContainers: mapset.NewSet[string](), + ruleBindingCache: ruleBindingCache, + objectCache: objectCache, + exporter: exporter, + metrics: metrics, + adapterFactory: adapterFactory, + enricher: enricher, + processManager: processManager, + ruleCooldown: ruleCooldown, + celEvaluator: celEvaluator, + ruleFailureCreator: ruleFailureCreator, + rulePolicyValidator: rulePolicyValidator, + mntnsRegistry: mntnsRegistry, + detectorManager: detectorManager, } return r, nil @@ -190,10 +190,16 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) profileExists = err == nil eventType := enrichedEvent.Event.GetEventType() + // Get current context string for filtering + contextStr := eventSourceContextToString(enrichedEvent.SourceContext) for _, rule := range rules { if !rule.Enabled { continue } + // Apply context-based filtering: rules only execute in declared contexts + if !RuleAppliesToContext(&rule, contextStr) { + continue + } // Skip profile dependency checks for non-K8s contexts (profiles are K8s-specific) // Only K8s contexts should enforce profile dependencies if isK8sContext && !profileExists && rule.ProfileDependency == armotypes.Required { @@ -424,6 +430,15 @@ func (rm *RuleManager) getUniqueIdAndMessage(enrichedEvent *events.EnrichedEvent } } +// eventSourceContextToString converts EventSourceContext to its string representation +func eventSourceContextToString(contextInfo contextdetection.ContextInfo) string { + if contextInfo == nil { + // Default to kubernetes when no context info is available + return "kubernetes" + } + return string(contextInfo.Context()) +} + func isSupportedEventType(rules []typesv1.Rule, enrichedEvent *events.EnrichedEvent) bool { eventType := enrichedEvent.Event.GetEventType() for _, rule := range rules { diff --git a/pkg/rulemanager/rulepolicy.go b/pkg/rulemanager/rulepolicy.go index b155d7a095..b394593bf1 100644 --- a/pkg/rulemanager/rulepolicy.go +++ b/pkg/rulemanager/rulepolicy.go @@ -2,8 +2,10 @@ package rulemanager import ( "slices" + "strings" "github.com/kubescape/node-agent/pkg/objectcache" + typesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" ) @@ -30,3 +32,38 @@ func (v *RulePolicyValidator) Validate(ruleId string, process string, ap *v1beta return false, nil } + +// RuleAppliesToContext checks if a rule should execute in the given context +// by checking the ExecutionContexts field first, then falling back to context: tags +func RuleAppliesToContext(rule *typesv1.Rule, currentContext string) bool { + // Try ExecutionContexts field first (preferred method) + if len(rule.ExecutionContexts) > 0 { + for _, ctx := range rule.ExecutionContexts { + if ctx == currentContext { + return true + } + } + return false + } + + // Fall back to parsing context: tags (for external service compatibility) + var contextTags []string + for _, tag := range rule.Tags { + if strings.HasPrefix(tag, "context:") { + ctx := strings.TrimPrefix(tag, "context:") + contextTags = append(contextTags, ctx) + } + } + + if len(contextTags) > 0 { + for _, ctx := range contextTags { + if ctx == currentContext { + return true + } + } + return false + } + + // No context specified in either field or tags: default to kubernetes only (backward compatible) + return currentContext == "kubernetes" +} diff --git a/pkg/rulemanager/types/v1/types.go b/pkg/rulemanager/types/v1/types.go index 120a162a0f..f9f657f529 100644 --- a/pkg/rulemanager/types/v1/types.go +++ b/pkg/rulemanager/types/v1/types.go @@ -27,6 +27,7 @@ type Rule struct { Severity int `json:"severity" yaml:"severity"` SupportPolicy bool `json:"supportPolicy" yaml:"supportPolicy"` Tags []string `json:"tags" yaml:"tags"` + ExecutionContexts []string `json:"executionContexts" yaml:"executionContexts"` // kubernetes, host, standalone State map[string]any `json:"state,omitempty" yaml:"state,omitempty"` AgentVersionRequirement string `json:"agentVersionRequirement" yaml:"agentVersionRequirement"` IsTriggerAlert bool `json:"isTriggerAlert" yaml:"isTriggerAlert"` diff --git a/pkg/utils/datasource_event.go b/pkg/utils/datasource_event.go index 143d90ad30..7317a9717b 100644 --- a/pkg/utils/datasource_event.go +++ b/pkg/utils/datasource_event.go @@ -88,7 +88,12 @@ func (e *DatasourceEvent) getFieldAccessor(fieldName string) datasource.FieldAcc } accessor, loaded := cache.(*sync.Map).Load(fieldName) if !loaded { - accessor, _ = cache.(*sync.Map).LoadOrStore(fieldName, e.Datasource.GetField(fieldName)) + field := e.Datasource.GetField(fieldName) + accessor, _ = cache.(*sync.Map).LoadOrStore(fieldName, field) + } + // Handle case where field doesn't exist (returns nil) + if accessor == nil { + return nil } return accessor.(datasource.FieldAccessor) } @@ -400,7 +405,11 @@ func (e *DatasourceEvent) GetModule() string { } func (e *DatasourceEvent) GetMountNsID() uint64 { - mountNsID, _ := e.getFieldAccessor("proc.mntns_id").Uint64(e.Data) + accessor := e.getFieldAccessor("proc.mntns_id") + if accessor == nil { + return 0 + } + mountNsID, _ := accessor.Uint64(e.Data) return mountNsID } From 30aac39022d02af1d07292cfec09bdfdd7741897 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Tue, 13 Jan 2026 14:49:01 +0200 Subject: [PATCH 03/17] move context conversion logic into RuleAppliesToContext --- pkg/rulemanager/rule_manager.go | 47 +++++++++++++-------------------- pkg/rulemanager/rulepolicy.go | 10 ++++++- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/pkg/rulemanager/rule_manager.go b/pkg/rulemanager/rule_manager.go index c0092dd51d..4002787189 100644 --- a/pkg/rulemanager/rule_manager.go +++ b/pkg/rulemanager/rule_manager.go @@ -88,23 +88,23 @@ func CreateRuleManager( detectorManager := detectors.NewDetectorManager(mntnsRegistry) r := &RuleManager{ - cfg: cfg, - ctx: ctx, - k8sClient: k8sClient, - trackedContainers: mapset.NewSet[string](), - ruleBindingCache: ruleBindingCache, - objectCache: objectCache, - exporter: exporter, - metrics: metrics, - adapterFactory: adapterFactory, - enricher: enricher, - processManager: processManager, - ruleCooldown: ruleCooldown, - celEvaluator: celEvaluator, - ruleFailureCreator: ruleFailureCreator, - rulePolicyValidator: rulePolicyValidator, - mntnsRegistry: mntnsRegistry, - detectorManager: detectorManager, + cfg: cfg, + ctx: ctx, + k8sClient: k8sClient, + trackedContainers: mapset.NewSet[string](), + ruleBindingCache: ruleBindingCache, + objectCache: objectCache, + exporter: exporter, + metrics: metrics, + adapterFactory: adapterFactory, + enricher: enricher, + processManager: processManager, + ruleCooldown: ruleCooldown, + celEvaluator: celEvaluator, + ruleFailureCreator: ruleFailureCreator, + rulePolicyValidator: rulePolicyValidator, + mntnsRegistry: mntnsRegistry, + detectorManager: detectorManager, } return r, nil @@ -190,14 +190,12 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) profileExists = err == nil eventType := enrichedEvent.Event.GetEventType() - // Get current context string for filtering - contextStr := eventSourceContextToString(enrichedEvent.SourceContext) for _, rule := range rules { if !rule.Enabled { continue } // Apply context-based filtering: rules only execute in declared contexts - if !RuleAppliesToContext(&rule, contextStr) { + if !RuleAppliesToContext(&rule, enrichedEvent.SourceContext) { continue } // Skip profile dependency checks for non-K8s contexts (profiles are K8s-specific) @@ -430,15 +428,6 @@ func (rm *RuleManager) getUniqueIdAndMessage(enrichedEvent *events.EnrichedEvent } } -// eventSourceContextToString converts EventSourceContext to its string representation -func eventSourceContextToString(contextInfo contextdetection.ContextInfo) string { - if contextInfo == nil { - // Default to kubernetes when no context info is available - return "kubernetes" - } - return string(contextInfo.Context()) -} - func isSupportedEventType(rules []typesv1.Rule, enrichedEvent *events.EnrichedEvent) bool { eventType := enrichedEvent.Event.GetEventType() for _, rule := range rules { diff --git a/pkg/rulemanager/rulepolicy.go b/pkg/rulemanager/rulepolicy.go index b394593bf1..1041b852af 100644 --- a/pkg/rulemanager/rulepolicy.go +++ b/pkg/rulemanager/rulepolicy.go @@ -4,6 +4,7 @@ import ( "slices" "strings" + "github.com/kubescape/node-agent/pkg/contextdetection" "github.com/kubescape/node-agent/pkg/objectcache" typesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" @@ -35,7 +36,14 @@ func (v *RulePolicyValidator) Validate(ruleId string, process string, ap *v1beta // RuleAppliesToContext checks if a rule should execute in the given context // by checking the ExecutionContexts field first, then falling back to context: tags -func RuleAppliesToContext(rule *typesv1.Rule, currentContext string) bool { +func RuleAppliesToContext(rule *typesv1.Rule, contextInfo contextdetection.ContextInfo) bool { + var currentContext string + if contextInfo == nil { + currentContext = string(contextdetection.Kubernetes) + } else { + currentContext = string(contextInfo.Context()) + } + // Try ExecutionContexts field first (preferred method) if len(rule.ExecutionContexts) > 0 { for _, ctx := range rule.ExecutionContexts { From 73a680e3e75357c56409031a993509ee90ed9332 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Tue, 13 Jan 2026 15:05:58 +0200 Subject: [PATCH 04/17] remove redundant comments --- pkg/contextdetection/detectors/host.go | 3 --- pkg/contextdetection/detectors/k8s.go | 2 -- pkg/contextdetection/detectors/standalone.go | 6 ------ pkg/rulemanager/rule_manager.go | 3 --- pkg/rulemanager/rulepolicy.go | 1 - 5 files changed, 15 deletions(-) diff --git a/pkg/contextdetection/detectors/host.go b/pkg/contextdetection/detectors/host.go index 5abdc8808d..43b9c98093 100644 --- a/pkg/contextdetection/detectors/host.go +++ b/pkg/contextdetection/detectors/host.go @@ -15,9 +15,6 @@ func (h *HostContextInfo) Context() contextdetection.EventSourceContext { return contextdetection.Host } -// WorkloadID returns the hostname as the workload identifier for host contexts. -// Returns the system hostname if available, or "host" as a fallback. -// This identifier is unique and deterministic within host contexts. func (h *HostContextInfo) WorkloadID() string { if h.HostName != "" { return h.HostName diff --git a/pkg/contextdetection/detectors/k8s.go b/pkg/contextdetection/detectors/k8s.go index 2a263a3b5b..ece4f69dfb 100644 --- a/pkg/contextdetection/detectors/k8s.go +++ b/pkg/contextdetection/detectors/k8s.go @@ -18,8 +18,6 @@ func (k *K8sContextInfo) Context() contextdetection.EventSourceContext { return contextdetection.Kubernetes } -// WorkloadID returns the Kubernetes workload identifier in the format "namespace/podname". -// This format is standardized and deterministic within Kubernetes contexts. func (k *K8sContextInfo) WorkloadID() string { return fmt.Sprintf("%s/%s", k.Namespace, k.PodName) } diff --git a/pkg/contextdetection/detectors/standalone.go b/pkg/contextdetection/detectors/standalone.go index 67bbaa31b3..ef2f8bdd19 100644 --- a/pkg/contextdetection/detectors/standalone.go +++ b/pkg/contextdetection/detectors/standalone.go @@ -14,12 +14,6 @@ func (s *StandaloneContextInfo) Context() contextdetection.EventSourceContext { return contextdetection.Standalone } -// WorkloadID returns a unique identifier for standalone containers. -// Format priority: -// 1. "name:containerID" if both name and ID are available -// 2. containerName if ID is empty -// 3. containerID if name is empty -// This identifier is deterministic and unique within standalone contexts. func (s *StandaloneContextInfo) WorkloadID() string { if s.ContainerID == "" { return s.ContainerName diff --git a/pkg/rulemanager/rule_manager.go b/pkg/rulemanager/rule_manager.go index 4002787189..a301393206 100644 --- a/pkg/rulemanager/rule_manager.go +++ b/pkg/rulemanager/rule_manager.go @@ -147,7 +147,6 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) isK8sContext := enrichedEvent.SourceContext == nil || enrichedEvent.SourceContext.Context() == contextdetection.Kubernetes if isK8sContext { - // K8s context: use pod-based workload ID from cache if pod == "" || namespace == "" { return } @@ -171,7 +170,6 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) // Retrieve rules based on context: K8s uses pod-based bindings, non-K8s uses all rules var rules []typesv1.Rule if enrichedEvent.SourceContext == nil || enrichedEvent.SourceContext.Context() == contextdetection.Kubernetes { - // K8s context: use pod-based rule bindings rules = rm.ruleBindingCache.ListRulesForPod(namespace, pod) } else { // Host or Standalone context: retrieve all rules (rule filtering deferred to future phases) @@ -194,7 +192,6 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) if !rule.Enabled { continue } - // Apply context-based filtering: rules only execute in declared contexts if !RuleAppliesToContext(&rule, enrichedEvent.SourceContext) { continue } diff --git a/pkg/rulemanager/rulepolicy.go b/pkg/rulemanager/rulepolicy.go index 1041b852af..b2f0771084 100644 --- a/pkg/rulemanager/rulepolicy.go +++ b/pkg/rulemanager/rulepolicy.go @@ -44,7 +44,6 @@ func RuleAppliesToContext(rule *typesv1.Rule, contextInfo contextdetection.Conte currentContext = string(contextInfo.Context()) } - // Try ExecutionContexts field first (preferred method) if len(rule.ExecutionContexts) > 0 { for _, ctx := range rule.ExecutionContexts { if ctx == currentContext { From 956b83706ff6a838fb23d057a40efe4551413300 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Tue, 13 Jan 2026 17:43:09 +0200 Subject: [PATCH 05/17] alert structure # Conflicts: # pkg/rulemanager/types/failure.go --- pkg/rulemanager/ruleadapters/creator.go | 44 +++++++++++++++++++++++++ pkg/rulemanager/types/failure.go | 37 +++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/pkg/rulemanager/ruleadapters/creator.go b/pkg/rulemanager/ruleadapters/creator.go index 450ed65ba8..3c9efbdd37 100644 --- a/pkg/rulemanager/ruleadapters/creator.go +++ b/pkg/rulemanager/ruleadapters/creator.go @@ -3,6 +3,7 @@ package ruleadapters import ( "errors" "fmt" + "os" "path/filepath" "reflect" "time" @@ -15,6 +16,7 @@ import ( "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" + "github.com/kubescape/node-agent/pkg/contextdetection" "github.com/kubescape/node-agent/pkg/dnsmanager" "github.com/kubescape/node-agent/pkg/ebpf/events" "github.com/kubescape/node-agent/pkg/objectcache" @@ -87,6 +89,7 @@ func (r *RuleFailureCreator) CreateRuleFailure(rule typesv1.Rule, enrichedEvent r.setRuntimeAlertK8sDetails(ruleFailure, objectCache) r.setCloudServices(ruleFailure) r.setProfileMetadata(rule, ruleFailure, objectCache) + r.setContextSpecificFields(ruleFailure, enrichedEvent) r.enrichRuleFailure(ruleFailure) if enrichedEvent.ProcessTree.PID != 0 { @@ -297,3 +300,44 @@ func (r *RuleFailureCreator) setRuntimeAlertK8sDetails(ruleFailure *types.Generi ruleFailure.SetRuntimeAlertK8sDetails(runtimek8sdetails) } + +func (r *RuleFailureCreator) setContextSpecificFields(ruleFailure *types.GenericRuleFailure, enrichedEvent *events.EnrichedEvent) { + // If no source context is available, default to Kubernetes (backward compatible) + if enrichedEvent.SourceContext == nil { + ruleFailure.SetSourceContext("kubernetes") + return + } + + sourceContextType := enrichedEvent.SourceContext.Context() + k8sDetails := ruleFailure.GetRuntimeAlertK8sDetails() + + switch sourceContextType { + case contextdetection.Kubernetes: + ruleFailure.SetSourceContext("kubernetes") + // K8s alerts use existing K8s details populated by setRuntimeAlertK8sDetails + + case contextdetection.Host: + ruleFailure.SetSourceContext("host") + // For Host context, use NodeName to store the hostname + hostname, err := os.Hostname() + if err == nil { + k8sDetails.NodeName = hostname + } + + case contextdetection.Standalone: + ruleFailure.SetSourceContext("docker") + // For Standalone context, populate container-specific fields in RuntimeAlertK8sDetails + if k8sDetails.ContainerID == "" { + k8sDetails.ContainerID = enrichedEvent.ContainerID + } + if k8sDetails.Image == "" { + k8sDetails.Image = ruleFailure.GetTriggerEvent().GetContainerImage() + } + // Use container name from trigger event if available + if k8sDetails.ContainerName == "" { + k8sDetails.ContainerName = ruleFailure.GetTriggerEvent().GetContainer() + } + } + + ruleFailure.SetRuntimeAlertK8sDetails(k8sDetails) +} diff --git a/pkg/rulemanager/types/failure.go b/pkg/rulemanager/types/failure.go index 14380fea51..555a3b840e 100644 --- a/pkg/rulemanager/types/failure.go +++ b/pkg/rulemanager/types/failure.go @@ -11,6 +11,29 @@ const ( NetworkProfile = "networkprofile" ) +// HostDetails contains metadata for Host context alerts. +// Used when a rule is triggered from the Host execution context (non-containerized). +// Provides system-level information to identify and contextualize host-level threats. +type HostDetails struct { + Hostname string + HostIP string + OS string + OSVersion string + KernelVersion string +} + +// DockerDetails contains metadata for Standalone/Docker container context alerts. +// Used when a rule is triggered from a standalone (non-Kubernetes) container. +// Provides container-level information to identify and contextualize container-specific threats. +type DockerDetails struct { + ContainerID string + ContainerName string + ImageID string + ImageName string + ImageTag string + NetworkMode string +} + type GenericRuleFailure struct { BaseRuntimeAlert apitypes.BaseRuntimeAlert AlertType apitypes.AlertType @@ -24,6 +47,8 @@ type GenericRuleFailure struct { HttpRuleAlert apitypes.HttpRuleAlert Extra interface{} IsTriggerAlert bool + // SourceContext identifies the execution context: "kubernetes", "host", "docker" + SourceContext string } type RuleFailure interface { @@ -49,6 +74,8 @@ type RuleFailure interface { GetAlertPlatform() apitypes.AlertSourcePlatform // Get Extra GetExtra() interface{} + // Get Source Context + GetSourceContext() string // Set Workload Details SetWorkloadDetails(workloadDetails string) @@ -74,6 +101,8 @@ type RuleFailure interface { GetIsTriggerAlert() bool // Set IsTriggerAlert SetIsTriggerAlert(isTriggerAlert bool) + // Set Source Context + SetSourceContext(sourceContext string) } func (rule *GenericRuleFailure) GetBaseRuntimeAlert() apitypes.BaseRuntimeAlert { @@ -175,3 +204,11 @@ func (rule *GenericRuleFailure) GetIsTriggerAlert() bool { func (rule *GenericRuleFailure) SetIsTriggerAlert(isTriggerAlert bool) { rule.IsTriggerAlert = isTriggerAlert } + +func (rule *GenericRuleFailure) GetSourceContext() string { + return rule.SourceContext +} + +func (rule *GenericRuleFailure) SetSourceContext(sourceContext string) { + rule.SourceContext = sourceContext +} From 58362e72cb745182b03daf25fc639291ef0bb0c9 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Tue, 13 Jan 2026 18:38:10 +0200 Subject: [PATCH 06/17] refactor(03-01): use 'standalone' instead of 'docker' for context naming Update SourceContext values to use consistent terminology: - "kubernetes" for Kubernetes context - "host" for Host context - "standalone" for Standalone/Docker container context Aligns with contextdetection.Standalone enum naming and improves clarity that this context supports any container runtime, not just Docker. --- pkg/rulemanager/ruleadapters/creator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/rulemanager/ruleadapters/creator.go b/pkg/rulemanager/ruleadapters/creator.go index 3c9efbdd37..e1eb31949a 100644 --- a/pkg/rulemanager/ruleadapters/creator.go +++ b/pkg/rulemanager/ruleadapters/creator.go @@ -325,7 +325,7 @@ func (r *RuleFailureCreator) setContextSpecificFields(ruleFailure *types.Generic } case contextdetection.Standalone: - ruleFailure.SetSourceContext("docker") + ruleFailure.SetSourceContext("standalone") // For Standalone context, populate container-specific fields in RuntimeAlertK8sDetails if k8sDetails.ContainerID == "" { k8sDetails.ContainerID = enrichedEvent.ContainerID From 12892aee640cbf74c621f1766530931de17ad93a Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Wed, 14 Jan 2026 08:47:03 +0200 Subject: [PATCH 07/17] use eventsourcecontext type --- pkg/rulemanager/ruleadapters/creator.go | 8 +++--- pkg/rulemanager/types/failure.go | 35 +++++-------------------- 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/pkg/rulemanager/ruleadapters/creator.go b/pkg/rulemanager/ruleadapters/creator.go index e1eb31949a..a3fdbc6336 100644 --- a/pkg/rulemanager/ruleadapters/creator.go +++ b/pkg/rulemanager/ruleadapters/creator.go @@ -304,7 +304,7 @@ func (r *RuleFailureCreator) setRuntimeAlertK8sDetails(ruleFailure *types.Generi func (r *RuleFailureCreator) setContextSpecificFields(ruleFailure *types.GenericRuleFailure, enrichedEvent *events.EnrichedEvent) { // If no source context is available, default to Kubernetes (backward compatible) if enrichedEvent.SourceContext == nil { - ruleFailure.SetSourceContext("kubernetes") + ruleFailure.SetSourceContext(contextdetection.Kubernetes) return } @@ -313,11 +313,11 @@ func (r *RuleFailureCreator) setContextSpecificFields(ruleFailure *types.Generic switch sourceContextType { case contextdetection.Kubernetes: - ruleFailure.SetSourceContext("kubernetes") + ruleFailure.SetSourceContext(contextdetection.Kubernetes) // K8s alerts use existing K8s details populated by setRuntimeAlertK8sDetails case contextdetection.Host: - ruleFailure.SetSourceContext("host") + ruleFailure.SetSourceContext(contextdetection.Host) // For Host context, use NodeName to store the hostname hostname, err := os.Hostname() if err == nil { @@ -325,7 +325,7 @@ func (r *RuleFailureCreator) setContextSpecificFields(ruleFailure *types.Generic } case contextdetection.Standalone: - ruleFailure.SetSourceContext("standalone") + ruleFailure.SetSourceContext(contextdetection.Standalone) // For Standalone context, populate container-specific fields in RuntimeAlertK8sDetails if k8sDetails.ContainerID == "" { k8sDetails.ContainerID = enrichedEvent.ContainerID diff --git a/pkg/rulemanager/types/failure.go b/pkg/rulemanager/types/failure.go index 555a3b840e..8bd5682de3 100644 --- a/pkg/rulemanager/types/failure.go +++ b/pkg/rulemanager/types/failure.go @@ -3,6 +3,7 @@ package types import ( apitypes "github.com/armosec/armoapi-go/armotypes" "github.com/armosec/utils-k8s-go/wlid" + "github.com/kubescape/node-agent/pkg/contextdetection" "github.com/kubescape/node-agent/pkg/utils" ) @@ -11,29 +12,6 @@ const ( NetworkProfile = "networkprofile" ) -// HostDetails contains metadata for Host context alerts. -// Used when a rule is triggered from the Host execution context (non-containerized). -// Provides system-level information to identify and contextualize host-level threats. -type HostDetails struct { - Hostname string - HostIP string - OS string - OSVersion string - KernelVersion string -} - -// DockerDetails contains metadata for Standalone/Docker container context alerts. -// Used when a rule is triggered from a standalone (non-Kubernetes) container. -// Provides container-level information to identify and contextualize container-specific threats. -type DockerDetails struct { - ContainerID string - ContainerName string - ImageID string - ImageName string - ImageTag string - NetworkMode string -} - type GenericRuleFailure struct { BaseRuntimeAlert apitypes.BaseRuntimeAlert AlertType apitypes.AlertType @@ -47,8 +25,7 @@ type GenericRuleFailure struct { HttpRuleAlert apitypes.HttpRuleAlert Extra interface{} IsTriggerAlert bool - // SourceContext identifies the execution context: "kubernetes", "host", "docker" - SourceContext string + SourceContext contextdetection.EventSourceContext } type RuleFailure interface { @@ -75,7 +52,7 @@ type RuleFailure interface { // Get Extra GetExtra() interface{} // Get Source Context - GetSourceContext() string + GetSourceContext() contextdetection.EventSourceContext // Set Workload Details SetWorkloadDetails(workloadDetails string) @@ -102,7 +79,7 @@ type RuleFailure interface { // Set IsTriggerAlert SetIsTriggerAlert(isTriggerAlert bool) // Set Source Context - SetSourceContext(sourceContext string) + SetSourceContext(sourceContext contextdetection.EventSourceContext) } func (rule *GenericRuleFailure) GetBaseRuntimeAlert() apitypes.BaseRuntimeAlert { @@ -205,10 +182,10 @@ func (rule *GenericRuleFailure) SetIsTriggerAlert(isTriggerAlert bool) { rule.IsTriggerAlert = isTriggerAlert } -func (rule *GenericRuleFailure) GetSourceContext() string { +func (rule *GenericRuleFailure) GetSourceContext() contextdetection.EventSourceContext { return rule.SourceContext } -func (rule *GenericRuleFailure) SetSourceContext(sourceContext string) { +func (rule *GenericRuleFailure) SetSourceContext(sourceContext contextdetection.EventSourceContext) { rule.SourceContext = sourceContext } From 2a2cea28b384603aff691baa23d3c0c0d6209d85 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Wed, 14 Jan 2026 10:14:04 +0200 Subject: [PATCH 08/17] use context type Signed-off-by: Yakir Oren --- pkg/contextdetection/detectors/manager.go | 11 ++++------ pkg/contextdetection/registry.go | 1 - pkg/rulemanager/containercallbacks.go | 3 +-- pkg/rulemanager/rule_manager.go | 26 +++++++++++------------ pkg/rulemanager/rulepolicy.go | 12 +++++------ 5 files changed, 23 insertions(+), 30 deletions(-) diff --git a/pkg/contextdetection/detectors/manager.go b/pkg/contextdetection/detectors/manager.go index 3eea697acf..6701410dd7 100644 --- a/pkg/contextdetection/detectors/manager.go +++ b/pkg/contextdetection/detectors/manager.go @@ -9,8 +9,10 @@ import ( "github.com/kubescape/node-agent/pkg/contextdetection" ) -// DetectorManager orchestrates context detection across multiple detectors. -// It executes detectors in priority order and returns the first match. +var ( + ErrInvalidContainer = errors.New("invalid container: nil provided") +) + type DetectorManager struct { registry *contextdetection.MntnsRegistry detectors []contextdetection.ContextDetector @@ -63,8 +65,3 @@ func (dm *DetectorManager) DetectContext(container *containercollection.Containe ContainerName: container.Runtime.ContainerName, }, nil } - -var ( - // ErrInvalidContainer is returned when a nil container is provided to DetectContext - ErrInvalidContainer = errors.New("invalid container: nil provided") -) diff --git a/pkg/contextdetection/registry.go b/pkg/contextdetection/registry.go index 90953ab24b..60ffa22551 100644 --- a/pkg/contextdetection/registry.go +++ b/pkg/contextdetection/registry.go @@ -14,7 +14,6 @@ var ( ) // MntnsRegistry maps mount namespace IDs to their context information. -// It uses SafeMap for thread-safe concurrent access. type MntnsRegistry struct { entries maps.SafeMap[uint64, ContextInfo] hostMntns uint64 diff --git a/pkg/rulemanager/containercallbacks.go b/pkg/rulemanager/containercallbacks.go index f9415a4596..4e12048d0c 100644 --- a/pkg/rulemanager/containercallbacks.go +++ b/pkg/rulemanager/containercallbacks.go @@ -62,7 +62,7 @@ func (rm *RuleManager) ContainerCallback(notif containercollection.PubSubEvent) return } - // Detect context and register in the registry + // Detect context and add to the registry contextInfo, err := rm.detectorManager.DetectContext(notif.Container) if err != nil { logger.L().Warning("RuleManager - failed to detect context", @@ -90,7 +90,6 @@ func (rm *RuleManager) ContainerCallback(notif containercollection.PubSubEvent) helpers.String("container ID", notif.Container.Runtime.ContainerID), helpers.String("k8s workload", k8sContainerID)) - // Unregister from the context registry if notif.Container.Mntns != 0 { rm.mntnsRegistry.Unregister(notif.Container.Mntns) } diff --git a/pkg/rulemanager/rule_manager.go b/pkg/rulemanager/rule_manager.go index a301393206..6de814d89e 100644 --- a/pkg/rulemanager/rule_manager.go +++ b/pkg/rulemanager/rule_manager.go @@ -83,8 +83,6 @@ func CreateRuleManager( ) (*RuleManager, error) { ruleFailureCreator := ruleadapters.NewRuleFailureCreator(enricher, dnsManager, adapterFactory) rulePolicyValidator := NewRulePolicyValidator(objectCache) - - // Create a detector manager for context detection detectorManager := detectors.NewDetectorManager(mntnsRegistry) r := &RuleManager{ @@ -135,7 +133,6 @@ func (rm *RuleManager) startRuleManager(container *containercollection.Container } func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) { - // Enrich event with source context and mount namespace ID for context-aware rule evaluation rm.enrichEventWithContext(enrichedEvent) var profileExists bool @@ -143,7 +140,7 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) namespace := enrichedEvent.Event.GetNamespace() pod := enrichedEvent.Event.GetPod() - // Determine workload ID based on context type + // Determine workload ID based on the context type isK8sContext := enrichedEvent.SourceContext == nil || enrichedEvent.SourceContext.Context() == contextdetection.Kubernetes if isK8sContext { @@ -167,12 +164,12 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) } } - // Retrieve rules based on context: K8s uses pod-based bindings, non-K8s uses all rules + // Retrieve rules based on context: K8s uses pod-based bindings var rules []typesv1.Rule if enrichedEvent.SourceContext == nil || enrichedEvent.SourceContext.Context() == contextdetection.Kubernetes { rules = rm.ruleBindingCache.ListRulesForPod(namespace, pod) } else { - // Host or Standalone context: retrieve all rules (rule filtering deferred to future phases) + // TODO: rule filtering based on context rules = rm.ruleBindingCache.GetRuleCreator().CreateAllRules() } @@ -192,6 +189,7 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) if !rule.Enabled { continue } + // TODO: for now we check each rule if it applies to the current context if !RuleAppliesToContext(&rule, enrichedEvent.SourceContext) { continue } @@ -261,14 +259,14 @@ func (rm *RuleManager) enrichEventWithContext(enrichedEvent *events.EnrichedEven } enrichedEvent.MountNamespaceID = mntnsID - // Look up context information for this mount namespace - if mntnsID > 0 { - if contextInfo, found := rm.mntnsRegistry.Lookup(mntnsID); found { - enrichedEvent.SourceContext = contextInfo - logger.L().Debug("RuleManager - enriched event with context", - helpers.String("mntns", fmt.Sprintf("%d", mntnsID)), - helpers.String("context", string(contextInfo.Context()))) - } + if mntnsID == 0 { + return + } + if contextInfo, found := rm.mntnsRegistry.Lookup(mntnsID); found { + enrichedEvent.SourceContext = contextInfo + logger.L().Debug("RuleManager - enriched event with context", + helpers.String("mntns", fmt.Sprintf("%d", mntnsID)), + helpers.String("context", string(contextInfo.Context()))) } } diff --git a/pkg/rulemanager/rulepolicy.go b/pkg/rulemanager/rulepolicy.go index b2f0771084..a4ce07b83f 100644 --- a/pkg/rulemanager/rulepolicy.go +++ b/pkg/rulemanager/rulepolicy.go @@ -37,16 +37,16 @@ func (v *RulePolicyValidator) Validate(ruleId string, process string, ap *v1beta // RuleAppliesToContext checks if a rule should execute in the given context // by checking the ExecutionContexts field first, then falling back to context: tags func RuleAppliesToContext(rule *typesv1.Rule, contextInfo contextdetection.ContextInfo) bool { - var currentContext string + var currentContext contextdetection.EventSourceContext if contextInfo == nil { - currentContext = string(contextdetection.Kubernetes) + currentContext = contextdetection.Kubernetes } else { - currentContext = string(contextInfo.Context()) + currentContext = contextInfo.Context() } if len(rule.ExecutionContexts) > 0 { for _, ctx := range rule.ExecutionContexts { - if ctx == currentContext { + if ctx == string(currentContext) { return true } } @@ -64,7 +64,7 @@ func RuleAppliesToContext(rule *typesv1.Rule, contextInfo contextdetection.Conte if len(contextTags) > 0 { for _, ctx := range contextTags { - if ctx == currentContext { + if ctx == string(currentContext) { return true } } @@ -72,5 +72,5 @@ func RuleAppliesToContext(rule *typesv1.Rule, contextInfo contextdetection.Conte } // No context specified in either field or tags: default to kubernetes only (backward compatible) - return currentContext == "kubernetes" + return currentContext == contextdetection.Kubernetes } From 473d1f0fff6e74e86f5cb35c746ebfac6d7fb2ef Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Wed, 14 Jan 2026 10:26:40 +0200 Subject: [PATCH 09/17] add configuration flags for host and standalone monitoring # Conflicts: # pkg/config/config.go --- configuration/config.json | 2 ++ pkg/config/config.go | 4 ++++ pkg/rulemanager/rule_manager.go | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/configuration/config.json b/configuration/config.json index 1319d54fb4..8f00b5cbc6 100644 --- a/configuration/config.json +++ b/configuration/config.json @@ -15,6 +15,8 @@ "seccompServiceEnabled": "false", "enableEmbeddedSBOMs": "false", "fimEnabled": true, + "hostMonitoringEnabled": false, + "standaloneMonitoringEnabled": false, "exporters": { "syslogExporterURL": "http://syslog.kubescape.svc.cluster.local:514", "stdoutExporter": "false", diff --git a/pkg/config/config.go b/pkg/config/config.go index 948ae5f288..c49f458155 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -66,6 +66,8 @@ type Config struct { EnableRuntimeDetection bool `mapstructure:"runtimeDetectionEnabled"` EnableSbomGeneration bool `mapstructure:"sbomGenerationEnabled"` EnableSeccomp bool `mapstructure:"seccompServiceEnabled"` + HostMonitoringEnabled bool `mapstructure:"hostMonitoringEnabled"` + StandaloneMonitoringEnabled bool `mapstructure:"standaloneMonitoringEnabled"` SeccompProfileBackend string `mapstructure:"seccompProfileBackend"` EventBatchSize int `mapstructure:"eventBatchSize"` ExcludeJsonPaths []string `mapstructure:"excludeJsonPaths"` @@ -179,6 +181,8 @@ func LoadConfig(path string) (Config, error) { viper.SetDefault("dnsCacheSize", 50000) viper.SetDefault("seccompProfileBackend", "storage") // "storage" or "crd" viper.SetDefault("containerEolNotificationBuffer", 100) + viper.SetDefault("hostMonitoringEnabled", false) + viper.SetDefault("standaloneMonitoringEnabled", false) // HTTP Exporter Alert Bulking defaults viper.SetDefault("exporters::httpExporterConfig::bulkMaxAlerts", 50) viper.SetDefault("exporters::httpExporterConfig::bulkTimeoutSeconds", 10) diff --git a/pkg/rulemanager/rule_manager.go b/pkg/rulemanager/rule_manager.go index 6de814d89e..ee59bb47d3 100644 --- a/pkg/rulemanager/rule_manager.go +++ b/pkg/rulemanager/rule_manager.go @@ -184,6 +184,11 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) _, apChecksum, err := profilehelper.GetContainerApplicationProfile(rm.objectCache, enrichedEvent.ContainerID) profileExists = err == nil + // Early exit if monitoring is disabled for this context - skip entire rule evaluation + if !rm.isContextMonitoringEnabled(enrichedEvent.SourceContext) { + return + } + eventType := enrichedEvent.Event.GetEventType() for _, rule := range rules { if !rule.Enabled { @@ -270,6 +275,26 @@ func (rm *RuleManager) enrichEventWithContext(enrichedEvent *events.EnrichedEven } } +func (rm *RuleManager) isContextMonitoringEnabled(sourceContext contextdetection.ContextInfo) bool { + if sourceContext == nil { + // No context information, default to Kubernetes (backward compatible) + return true + } + + contextType := sourceContext.Context() + switch contextType { + case contextdetection.Host: + return rm.cfg.HostMonitoringEnabled + case contextdetection.Standalone: + return rm.cfg.StandaloneMonitoringEnabled + case contextdetection.Kubernetes: + // Kubernetes monitoring is always enabled (backward compatible) + return true + default: + return true + } +} + func (rm *RuleManager) HasApplicableRuleBindings(namespace, name string) bool { return len(rm.ruleBindingCache.ListRulesForPod(namespace, name)) > 0 } From d45021fb3f28384934c4b1c05d8e1a8519c552ee Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Wed, 14 Jan 2026 13:40:20 +0200 Subject: [PATCH 10/17] rename func --- pkg/rulemanager/rule_manager.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/rulemanager/rule_manager.go b/pkg/rulemanager/rule_manager.go index ee59bb47d3..5b3840fe5a 100644 --- a/pkg/rulemanager/rule_manager.go +++ b/pkg/rulemanager/rule_manager.go @@ -184,8 +184,8 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) _, apChecksum, err := profilehelper.GetContainerApplicationProfile(rm.objectCache, enrichedEvent.ContainerID) profileExists = err == nil - // Early exit if monitoring is disabled for this context - skip entire rule evaluation - if !rm.isContextMonitoringEnabled(enrichedEvent.SourceContext) { + // Early exit if monitoring is disabled for this context - skip rule evaluation + if !rm.isMonitoringEnabledForContext(enrichedEvent.SourceContext) { return } @@ -194,7 +194,7 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) if !rule.Enabled { continue } - // TODO: for now we check each rule if it applies to the current context + if !RuleAppliesToContext(&rule, enrichedEvent.SourceContext) { continue } @@ -275,7 +275,7 @@ func (rm *RuleManager) enrichEventWithContext(enrichedEvent *events.EnrichedEven } } -func (rm *RuleManager) isContextMonitoringEnabled(sourceContext contextdetection.ContextInfo) bool { +func (rm *RuleManager) isMonitoringEnabledForContext(sourceContext contextdetection.ContextInfo) bool { if sourceContext == nil { // No context information, default to Kubernetes (backward compatible) return true From 3bcaf9d0b81b294e0c6a30a87c5656a3290d5998 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Wed, 14 Jan 2026 13:57:22 +0200 Subject: [PATCH 11/17] create Registry interface for mount namespace management --- .../v2/container_watcher_collection.go | 47 +++++++++++++++++++ pkg/contextdetection/hostutil.go | 21 --------- pkg/contextdetection/registry.go | 45 +++++++----------- 3 files changed, 64 insertions(+), 49 deletions(-) delete mode 100644 pkg/contextdetection/hostutil.go diff --git a/pkg/containerwatcher/v2/container_watcher_collection.go b/pkg/containerwatcher/v2/container_watcher_collection.go index 59451d82b2..18dc456a18 100644 --- a/pkg/containerwatcher/v2/container_watcher_collection.go +++ b/pkg/containerwatcher/v2/container_watcher_collection.go @@ -3,8 +3,10 @@ package containerwatcher import ( "context" "fmt" + "syscall" "time" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" "github.com/inspektor-gadget/inspektor-gadget/pkg/operators/socketenricher" "github.com/inspektor-gadget/inspektor-gadget/pkg/params" @@ -104,6 +106,27 @@ func (cw *ContainerWatcher) StartContainerCollection(ctx context.Context) error } logger.L().Info("ContainerManager - container collection initialized successfully") + // Create virtual host container if host monitoring enabled + if cw.cfg.HostMonitoringEnabled { + virtualHostContainer, err := getHostAsContainer() + if err != nil { + logger.L().Warning("ContainerManager - failed to create virtual host container", + helpers.Error(err)) + } else { + cw.containerCollection.AddContainer(virtualHostContainer) + + // Manually trigger callbacks to ensure context detection runs + cw.containerCallback(containercollection.PubSubEvent{ + Type: containercollection.EventTypeAddContainer, + Container: virtualHostContainer, + }) + + logger.L().Info("ContainerManager - virtual host container created", + helpers.String("mntns", fmt.Sprintf("%d", virtualHostContainer.Mntns)), + helpers.String("pid", fmt.Sprintf("%d", virtualHostContainer.Runtime.ContainerPID))) + } + } + // Start monitoring for rule bindings notifications go cw.startRunningContainers() @@ -148,3 +171,27 @@ func (cw *ContainerWatcher) addRunningContainers(notf *rulebindingmanager.RuleBi cw.ruleManagedPods.Remove(k8sPodID) } } + +// getHostAsContainer creates a synthetic container representing the host's mount namespace. +// This enables host-level events to be processed through the standard container infrastructure. +func getHostAsContainer() (*containercollection.Container, error) { + pidOne := 1 + + // Get the mount namespace ID for PID 1 (init process) + // This is done by reading the inode of /proc/1/ns/mnt + mntnsPath := fmt.Sprintf("/proc/%d/ns/mnt", pidOne) + + var statData syscall.Stat_t + if err := syscall.Stat(mntnsPath, &statData); err != nil { + return nil, fmt.Errorf("getting mount namespace for host PID %d: %w", pidOne, err) + } + + return &containercollection.Container{ + Runtime: containercollection.RuntimeMetadata{ + BasicRuntimeMetadata: eventtypes.BasicRuntimeMetadata{ + ContainerPID: uint32(pidOne), + }, + }, + Mntns: statData.Ino, + }, nil +} diff --git a/pkg/contextdetection/hostutil.go b/pkg/contextdetection/hostutil.go deleted file mode 100644 index fa692d6b21..0000000000 --- a/pkg/contextdetection/hostutil.go +++ /dev/null @@ -1,21 +0,0 @@ -package contextdetection - -import ( - "fmt" - - containerutils "github.com/inspektor-gadget/inspektor-gadget/pkg/container-utils" - "github.com/kubescape/go-logger" - "github.com/kubescape/go-logger/helpers" -) - -// GetCurrentHostMntns reads the host's mount namespace ID from PID 1 (init process). -func GetCurrentHostMntns() (uint64, error) { - mntns, err := containerutils.GetMntNs(1) - if err != nil { - logger.L().Error("failed to detect host mount namespace from PID 1", - helpers.Error(err)) - return 0, fmt.Errorf("failed to get host mount namespace: %w", err) - } - - return mntns, nil -} diff --git a/pkg/contextdetection/registry.go b/pkg/contextdetection/registry.go index 60ffa22551..55b8e7faa5 100644 --- a/pkg/contextdetection/registry.go +++ b/pkg/contextdetection/registry.go @@ -13,12 +13,29 @@ var ( ErrInvalidMntns = errors.New("invalid mount namespace ID: cannot be zero") ) +// Registry defines the interface for managing mount namespace to context mappings. +type Registry interface { + // Register adds or updates a mount namespace entry in the registry. + // Returns an error if the mount namespace ID is invalid (zero). + Register(mntns uint64, info ContextInfo) error + + // Lookup retrieves the context information for a mount namespace. + // Returns the context info and true if found, false otherwise. + Lookup(mntns uint64) (ContextInfo, bool) + + // Unregister removes a mount namespace entry from the registry. + Unregister(mntns uint64) +} + // MntnsRegistry maps mount namespace IDs to their context information. type MntnsRegistry struct { entries maps.SafeMap[uint64, ContextInfo] hostMntns uint64 } +// Verify that MntnsRegistry implements the Registry interface. +var _ Registry = (*MntnsRegistry)(nil) + // NewMntnsRegistry creates a new MntnsRegistry instance. // The hostMntns should be set separately via SetHostMntns after initialization. func NewMntnsRegistry() *MntnsRegistry { @@ -61,31 +78,3 @@ func (r *MntnsRegistry) Unregister(mntns uint64) { helpers.String("mntns", fmt.Sprintf("%d", mntns))) } } - -// SetHostMntns sets the host's mount namespace ID. -func (r *MntnsRegistry) SetHostMntns(mntns uint64) error { - if mntns == 0 { - return ErrInvalidMntns - } - - r.hostMntns = mntns - logger.L().Info("MntnsRegistry - host mount namespace set", - helpers.String("mntns", fmt.Sprintf("%d", mntns))) - - return nil -} - -// GetHostMntns returns the host's mount namespace ID. -func (r *MntnsRegistry) GetHostMntns() uint64 { - return r.hostMntns -} - -// IsHostMntns checks if the given mount namespace ID is the host's. -func (r *MntnsRegistry) IsHostMntns(mntns uint64) bool { - return r.hostMntns != 0 && mntns == r.hostMntns -} - -// Size returns the number of entries in the registry. -func (r *MntnsRegistry) Size() int { - return r.entries.Len() -} From 17f034a8d730d738f035153bad3be556487f4b77 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Thu, 15 Jan 2026 08:48:19 +0200 Subject: [PATCH 12/17] add virtual host container Signed-off-by: Yakir Oren --- cmd/main.go | 13 ------------- .../v2/container_watcher_collection.go | 11 ++++------- pkg/containerwatcher/v2/containercallback.go | 6 ++++++ pkg/rulemanager/rulepolicy.go | 11 ----------- pkg/rulemanager/types/v1/types.go | 1 - 5 files changed, 10 insertions(+), 32 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index c6e39be302..765f8a0234 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -291,21 +291,8 @@ func main() { logger.L().Ctx(ctx).Fatal("error creating CEL evaluator", helpers.Error(err)) } - // Create and initialize MntnsRegistry for host context monitoring mntnsRegistry := contextdetection.NewMntnsRegistry() - // Auto-detect and initialize host mount namespace - hostMntns, err := contextdetection.GetCurrentHostMntns() - if err != nil { - logger.L().Ctx(ctx).Warning("failed to detect host mount namespace", - helpers.Error(err)) - } else { - if err := mntnsRegistry.SetHostMntns(hostMntns); err != nil { - logger.L().Ctx(ctx).Warning("failed to set host mount namespace", - helpers.Error(err)) - } - } - // create runtimeDetection managers ruleManager, err = rulemanager.CreateRuleManager(ctx, cfg, k8sClient, ruleBindingCache, objCache, exporter, prometheusExporter, processTreeManager, dnsResolver, nil, ruleCooldown, adapterFactory, celEvaluator, mntnsRegistry) if err != nil { diff --git a/pkg/containerwatcher/v2/container_watcher_collection.go b/pkg/containerwatcher/v2/container_watcher_collection.go index 18dc456a18..ce7d9f3212 100644 --- a/pkg/containerwatcher/v2/container_watcher_collection.go +++ b/pkg/containerwatcher/v2/container_watcher_collection.go @@ -3,10 +3,10 @@ package containerwatcher import ( "context" "fmt" - "syscall" "time" eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" + containerutils "github.com/inspektor-gadget/inspektor-gadget/pkg/container-utils" containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" "github.com/inspektor-gadget/inspektor-gadget/pkg/operators/socketenricher" "github.com/inspektor-gadget/inspektor-gadget/pkg/params" @@ -178,11 +178,8 @@ func getHostAsContainer() (*containercollection.Container, error) { pidOne := 1 // Get the mount namespace ID for PID 1 (init process) - // This is done by reading the inode of /proc/1/ns/mnt - mntnsPath := fmt.Sprintf("/proc/%d/ns/mnt", pidOne) - - var statData syscall.Stat_t - if err := syscall.Stat(mntnsPath, &statData); err != nil { + mntns, err := containerutils.GetMntNs(pidOne) + if err != nil { return nil, fmt.Errorf("getting mount namespace for host PID %d: %w", pidOne, err) } @@ -192,6 +189,6 @@ func getHostAsContainer() (*containercollection.Container, error) { ContainerPID: uint32(pidOne), }, }, - Mntns: statData.Ino, + Mntns: mntns, }, nil } diff --git a/pkg/containerwatcher/v2/containercallback.go b/pkg/containerwatcher/v2/containercallback.go index 45aa3928ba..0fb5ce08cd 100644 --- a/pkg/containerwatcher/v2/containercallback.go +++ b/pkg/containerwatcher/v2/containercallback.go @@ -60,6 +60,12 @@ func (cw *ContainerWatcher) containerCallbackAsync(notif containercollection.Pub helpers.String("ContainerImageName", notif.Container.Runtime.ContainerImageName)) cw.metrics.ReportContainerStart() + // Skip shared data setup for virtual host container (identified by ContainerPID == 1) + if notif.Container.Runtime.ContainerPID == 1 { + logger.L().Debug("ContainerWatcher.containerCallback - skipping shared data setup for virtual host container") + return + } + // Set shared watched container data go cw.setSharedWatchedContainerData(notif.Container) case containercollection.EventTypeRemoveContainer: diff --git a/pkg/rulemanager/rulepolicy.go b/pkg/rulemanager/rulepolicy.go index a4ce07b83f..ca79b8fcd5 100644 --- a/pkg/rulemanager/rulepolicy.go +++ b/pkg/rulemanager/rulepolicy.go @@ -35,7 +35,6 @@ func (v *RulePolicyValidator) Validate(ruleId string, process string, ap *v1beta } // RuleAppliesToContext checks if a rule should execute in the given context -// by checking the ExecutionContexts field first, then falling back to context: tags func RuleAppliesToContext(rule *typesv1.Rule, contextInfo contextdetection.ContextInfo) bool { var currentContext contextdetection.EventSourceContext if contextInfo == nil { @@ -44,16 +43,6 @@ func RuleAppliesToContext(rule *typesv1.Rule, contextInfo contextdetection.Conte currentContext = contextInfo.Context() } - if len(rule.ExecutionContexts) > 0 { - for _, ctx := range rule.ExecutionContexts { - if ctx == string(currentContext) { - return true - } - } - return false - } - - // Fall back to parsing context: tags (for external service compatibility) var contextTags []string for _, tag := range rule.Tags { if strings.HasPrefix(tag, "context:") { diff --git a/pkg/rulemanager/types/v1/types.go b/pkg/rulemanager/types/v1/types.go index f9f657f529..120a162a0f 100644 --- a/pkg/rulemanager/types/v1/types.go +++ b/pkg/rulemanager/types/v1/types.go @@ -27,7 +27,6 @@ type Rule struct { Severity int `json:"severity" yaml:"severity"` SupportPolicy bool `json:"supportPolicy" yaml:"supportPolicy"` Tags []string `json:"tags" yaml:"tags"` - ExecutionContexts []string `json:"executionContexts" yaml:"executionContexts"` // kubernetes, host, standalone State map[string]any `json:"state,omitempty" yaml:"state,omitempty"` AgentVersionRequirement string `json:"agentVersionRequirement" yaml:"agentVersionRequirement"` IsTriggerAlert bool `json:"isTriggerAlert" yaml:"isTriggerAlert"` From bd1fc95734c6797739cc45261704ff9b5278fd12 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Thu, 15 Jan 2026 10:28:35 +0100 Subject: [PATCH 13/17] add changes from #682 Signed-off-by: Matthias Bertschy --- pkg/cloudmetadata/metadata.go | 179 +++++++++++++++++- pkg/containerwatcher/v2/tracers/bpf.go | 5 +- .../v2/tracers/capabilities.go | 1 + pkg/containerwatcher/v2/tracers/dns.go | 3 +- pkg/containerwatcher/v2/tracers/exec.go | 3 +- pkg/containerwatcher/v2/tracers/exit.go | 5 +- pkg/containerwatcher/v2/tracers/fork.go | 5 +- pkg/containerwatcher/v2/tracers/hardlink.go | 5 +- pkg/containerwatcher/v2/tracers/http.go | 5 +- pkg/containerwatcher/v2/tracers/iouring.go | 5 +- pkg/containerwatcher/v2/tracers/kmod.go | 5 +- pkg/containerwatcher/v2/tracers/network.go | 3 +- pkg/containerwatcher/v2/tracers/open.go | 7 +- pkg/containerwatcher/v2/tracers/ptrace.go | 5 +- pkg/containerwatcher/v2/tracers/randomx.go | 5 +- pkg/containerwatcher/v2/tracers/ssh.go | 5 +- pkg/containerwatcher/v2/tracers/symlink.go | 5 +- pkg/containerwatcher/v2/tracers/syscall.go | 1 + pkg/containerwatcher/v2/tracers/unshare.go | 5 +- pkg/utils/datasource_event.go | 3 + 20 files changed, 239 insertions(+), 21 deletions(-) diff --git a/pkg/cloudmetadata/metadata.go b/pkg/cloudmetadata/metadata.go index dd26d10d8a..7d3cee243a 100644 --- a/pkg/cloudmetadata/metadata.go +++ b/pkg/cloudmetadata/metadata.go @@ -3,6 +3,10 @@ package cloudmetadata import ( "context" "fmt" + "io" + "net/http" + "strings" + "time" apitypes "github.com/armosec/armoapi-go/armotypes" "github.com/kubescape/go-logger" @@ -54,8 +58,181 @@ func GetCloudMetadataWithIMDS(ctx context.Context) (*apitypes.CloudMetadata, err cMetadata, err := cMetadataClient.GetMetadata(ctx) if err != nil { - return nil, err + logger.L().Info("failed to get cloud metadata from IMDS, trying fallbacks", helpers.Error(err)) + + // Try DigitalOcean metadata endpoints as a fallback (e.g., droplets) if IMDS didn't work. + if doMeta, derr := fetchDigitalOceanMetadata(ctx); derr == nil && doMeta != nil { + logger.L().Info("retrieved cloud metadata from DigitalOcean metadata service") + return doMeta, nil + } + + // Try GCP metadata endpoints as a fallback + if gcpMeta, gerr := fetchGCPMetadata(ctx); gerr == nil && gcpMeta != nil { + logger.L().Info("retrieved cloud metadata from GCP metadata service") + return gcpMeta, nil + } + + // Try Azure metadata endpoints as a fallback + if azureMeta, aerr := fetchAzureMetadata(ctx); aerr == nil && azureMeta != nil { + logger.L().Info("retrieved cloud metadata from Azure metadata service") + return azureMeta, nil + } + + // Wrap the underlying error with additional context so logs make it clearer why metadata is missing. + // This helps surface issues like IMDS token endpoint failures (e.g. IMDSv2 token 404), unreachable metadata endpoints, + // or provider-specific metadata problems. + return nil, fmt.Errorf("failed to get cloud metadata from IMDS: %w", err) } return cMetadata, nil } + +// fetchDigitalOceanMetadata attempts to fetch basic metadata from DigitalOcean's metadata service. +// +// It probes the metadata root and queries a few commonly available endpoints. +// It returns a non-nil error if it does not look like DigitalOcean's metadata service or no useful values were found. +func fetchDigitalOceanMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { + client := &http.Client{ + Timeout: 2 * time.Second, + } + base := "http://169.254.169.254/metadata/v1/" + + // Probe root to see whether the metadata endpoint responds and contains expected entries. + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, base, nil) + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return nil, fmt.Errorf("digitalocean metadata root returned status: %d", resp.StatusCode) + } + body, _ := io.ReadAll(resp.Body) + bstr := string(body) + // Basic heuristic: the DO metadata root typically lists resources like 'id', 'hostname', 'region' etc. + if !strings.Contains(bstr, "id") && !strings.Contains(bstr, "region") && !strings.Contains(bstr, "hostname") { + return nil, fmt.Errorf("digitalocean metadata root missing expected entries") + } + + // helper to fetch a single textual endpoint and return trimmed result or empty string + get := func(path string) string { + url := base + path + r, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + resp2, err2 := client.Do(r) + if err2 != nil || resp2.StatusCode != 200 { + if resp2 != nil { + resp2.Body.Close() + } + return "" + } + defer resp2.Body.Close() + b, _ := io.ReadAll(resp2.Body) + return strings.TrimSpace(string(b)) + } + + id := get("id") + if id == "" { + id = get("droplet_id") + } + hostname := get("hostname") + region := get("region") + instanceType := get("size") + if instanceType == "" { + instanceType = get("type") + } + privateIP := get("interfaces/private/0/ipv4/address") + publicIP := get("interfaces/public/0/ipv4/address") + + // if nothing useful was obtained, return an error so callers can continue trying other fallbacks + if id == "" && hostname == "" && region == "" && privateIP == "" && publicIP == "" && instanceType == "" { + return nil, fmt.Errorf("digitalocean metadata endpoints returned no data") + } + + return &apitypes.CloudMetadata{ + Provider: "digitalocean", + InstanceID: id, + InstanceType: instanceType, + Region: region, + PrivateIP: privateIP, + PublicIP: publicIP, + Hostname: hostname, + }, nil +} + +// fetchGCPMetadata attempts to fetch basic metadata from GCP's metadata service. +func fetchGCPMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { + client := &http.Client{ + Timeout: 2 * time.Second, + } + base := "http://metadata.google.internal/computeMetadata/v1/instance/" + + get := func(path string) string { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, base+path, nil) + req.Header.Set("Metadata-Flavor", "Google") + resp, err := client.Do(req) + if err != nil || resp.StatusCode != 200 { + if resp != nil { + resp.Body.Close() + } + return "" + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + return strings.TrimSpace(string(b)) + } + + machineType := get("machine-type") + if machineType == "" { + return nil, fmt.Errorf("not a GCP instance") + } + + // GCP returns full path like "projects/12345/machineTypes/n1-standard-1" + parts := strings.Split(machineType, "/") + instanceType := parts[len(parts)-1] + + return &apitypes.CloudMetadata{ + Provider: "gcp", + InstanceID: get("id"), + InstanceType: instanceType, + Zone: get("zone"), + Hostname: get("hostname"), + }, nil +} + +// fetchAzureMetadata attempts to fetch basic metadata from Azure's metadata service. +func fetchAzureMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { + client := &http.Client{ + Timeout: 2 * time.Second, + } + base := "http://169.254.169.254/metadata/instance/compute/" + + get := func(path string) string { + url := base + path + "?api-version=2021-02-01&format=text" + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + req.Header.Set("Metadata", "true") + resp, err := client.Do(req) + if err != nil || resp.StatusCode != 200 { + if resp != nil { + resp.Body.Close() + } + return "" + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + return strings.TrimSpace(string(b)) + } + + vmSize := get("vmSize") + if vmSize == "" { + return nil, fmt.Errorf("not an Azure instance") + } + + return &apitypes.CloudMetadata{ + Provider: "azure", + InstanceID: get("vmId"), + InstanceType: vmSize, + Region: get("location"), + Zone: get("zone"), + Hostname: get("name"), + }, nil +} diff --git a/pkg/containerwatcher/v2/tracers/bpf.go b/pkg/containerwatcher/v2/tracers/bpf.go index 87f57b8a3f..d74c345035 100644 --- a/pkg/containerwatcher/v2/tracers/bpf.go +++ b/pkg/containerwatcher/v2/tracers/bpf.go @@ -68,7 +68,10 @@ func (bt *BpfTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(bt.ociStore), ) go func() { - err := bt.runtime.RunGadget(bt.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := bt.runtime.RunGadget(bt.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", bt.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/capabilities.go b/pkg/containerwatcher/v2/tracers/capabilities.go index 74bd0b3d74..b00cc1dc5c 100644 --- a/pkg/containerwatcher/v2/tracers/capabilities.go +++ b/pkg/containerwatcher/v2/tracers/capabilities.go @@ -70,6 +70,7 @@ func (ct *CapabilitiesTracer) Start(ctx context.Context) error { params := map[string]string{ "operator.oci.ebpf.collect-kstack": "false", "operator.oci.ebpf.unique": "true", + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager } err := ct.runtime.RunGadget(ct.gadgetCtx, nil, params) if err != nil { diff --git a/pkg/containerwatcher/v2/tracers/dns.go b/pkg/containerwatcher/v2/tracers/dns.go index 0fd3dd0d9f..215e2df70f 100644 --- a/pkg/containerwatcher/v2/tracers/dns.go +++ b/pkg/containerwatcher/v2/tracers/dns.go @@ -74,7 +74,8 @@ func (dt *DNSTracer) Start(ctx context.Context) error { ) go func() { params := map[string]string{ - "operator.oci.ebpf.paths": "true", // CWD paths in events + "operator.oci.ebpf.paths": "true", // CWD paths in events + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager } err := dt.runtime.RunGadget(dt.gadgetCtx, nil, params) if err != nil { diff --git a/pkg/containerwatcher/v2/tracers/exec.go b/pkg/containerwatcher/v2/tracers/exec.go index 21a414a90c..6c25026ef1 100644 --- a/pkg/containerwatcher/v2/tracers/exec.go +++ b/pkg/containerwatcher/v2/tracers/exec.go @@ -69,7 +69,8 @@ func (et *ExecTracer) Start(ctx context.Context) error { ) go func() { params := map[string]string{ - "operator.oci.ebpf.paths": "true", // CWD paths in events + "operator.oci.ebpf.paths": "true", // CWD paths in events + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager } err := et.runtime.RunGadget(et.gadgetCtx, nil, params) if err != nil { diff --git a/pkg/containerwatcher/v2/tracers/exit.go b/pkg/containerwatcher/v2/tracers/exit.go index 03b0a3a72e..cdf9b2dd15 100644 --- a/pkg/containerwatcher/v2/tracers/exit.go +++ b/pkg/containerwatcher/v2/tracers/exit.go @@ -64,7 +64,10 @@ func (et *ExitTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(et.ociStore), ) go func() { - err := et.runtime.RunGadget(et.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := et.runtime.RunGadget(et.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", et.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/fork.go b/pkg/containerwatcher/v2/tracers/fork.go index d31d133d7f..d06b159e5b 100644 --- a/pkg/containerwatcher/v2/tracers/fork.go +++ b/pkg/containerwatcher/v2/tracers/fork.go @@ -64,7 +64,10 @@ func (ft *ForkTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(ft.ociStore), ) go func() { - err := ft.runtime.RunGadget(ft.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := ft.runtime.RunGadget(ft.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", ft.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/hardlink.go b/pkg/containerwatcher/v2/tracers/hardlink.go index be0558af7a..a3dfa85ecc 100644 --- a/pkg/containerwatcher/v2/tracers/hardlink.go +++ b/pkg/containerwatcher/v2/tracers/hardlink.go @@ -68,7 +68,10 @@ func (ht *HardlinkTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(ht.ociStore), ) go func() { - err := ht.runtime.RunGadget(ht.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := ht.runtime.RunGadget(ht.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", ht.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/http.go b/pkg/containerwatcher/v2/tracers/http.go index 36f3e5b7e2..3577c7e7f9 100644 --- a/pkg/containerwatcher/v2/tracers/http.go +++ b/pkg/containerwatcher/v2/tracers/http.go @@ -82,7 +82,10 @@ func (ht *HTTPTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(ht.ociStore), ) go func() { - err := ht.runtime.RunGadget(ht.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := ht.runtime.RunGadget(ht.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", ht.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/iouring.go b/pkg/containerwatcher/v2/tracers/iouring.go index 8977fc396c..2f23b27fd1 100644 --- a/pkg/containerwatcher/v2/tracers/iouring.go +++ b/pkg/containerwatcher/v2/tracers/iouring.go @@ -82,7 +82,10 @@ func (it *IoUringTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(it.ociStore), ) go func() { - err := it.runtime.RunGadget(it.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := it.runtime.RunGadget(it.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", it.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/kmod.go b/pkg/containerwatcher/v2/tracers/kmod.go index 6d02d06143..6eaa373764 100644 --- a/pkg/containerwatcher/v2/tracers/kmod.go +++ b/pkg/containerwatcher/v2/tracers/kmod.go @@ -68,7 +68,10 @@ func (kt *KmodTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(kt.ociStore), ) go func() { - err := kt.runtime.RunGadget(kt.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := kt.runtime.RunGadget(kt.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", kt.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/network.go b/pkg/containerwatcher/v2/tracers/network.go index c5c36537b1..e042203a55 100644 --- a/pkg/containerwatcher/v2/tracers/network.go +++ b/pkg/containerwatcher/v2/tracers/network.go @@ -83,7 +83,8 @@ func (nt *NetworkTracer) Start(ctx context.Context) error { ) go func() { params := map[string]string{ - "operator.oci.annotate": "network:kubenameresolver.enable=true", + "operator.oci.annotate": "network:kubenameresolver.enable=true", + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager } err := nt.runtime.RunGadget(nt.gadgetCtx, nil, params) if err != nil { diff --git a/pkg/containerwatcher/v2/tracers/open.go b/pkg/containerwatcher/v2/tracers/open.go index 4b5b6e2106..210dd5b7e5 100644 --- a/pkg/containerwatcher/v2/tracers/open.go +++ b/pkg/containerwatcher/v2/tracers/open.go @@ -70,7 +70,8 @@ func (ot *OpenTracer) Start(ctx context.Context) error { ) go func() { params := map[string]string{ - "operator.oci.ebpf.paths": strconv.FormatBool(ot.cfg.EnableFullPathTracing), + "operator.oci.ebpf.paths": strconv.FormatBool(ot.cfg.EnableFullPathTracing), + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager } err := ot.runtime.RunGadget(ot.gadgetCtx, nil, params) if err != nil { @@ -128,10 +129,6 @@ func (ot *OpenTracer) eventOperator() operators.DataOperator { // callback handles open events from the tracer func (ot *OpenTracer) callback(event utils.OpenEvent) { - if event.GetContainer() == "" { - return - } - errorRaw := event.GetError() if errorRaw > -1 { // Handle the event with syscall enrichment diff --git a/pkg/containerwatcher/v2/tracers/ptrace.go b/pkg/containerwatcher/v2/tracers/ptrace.go index d01625eb16..68201e686f 100644 --- a/pkg/containerwatcher/v2/tracers/ptrace.go +++ b/pkg/containerwatcher/v2/tracers/ptrace.go @@ -64,7 +64,10 @@ func (pt *PtraceTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(pt.ociStore), ) go func() { - err := pt.runtime.RunGadget(pt.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := pt.runtime.RunGadget(pt.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", pt.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/randomx.go b/pkg/containerwatcher/v2/tracers/randomx.go index dd6b6bba36..eea8d9a9f4 100644 --- a/pkg/containerwatcher/v2/tracers/randomx.go +++ b/pkg/containerwatcher/v2/tracers/randomx.go @@ -65,7 +65,10 @@ func (rt *RandomXTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(rt.ociStore), ) go func() { - err := rt.runtime.RunGadget(rt.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := rt.runtime.RunGadget(rt.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", rt.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/ssh.go b/pkg/containerwatcher/v2/tracers/ssh.go index c29157dc01..c6e9f899ea 100644 --- a/pkg/containerwatcher/v2/tracers/ssh.go +++ b/pkg/containerwatcher/v2/tracers/ssh.go @@ -69,7 +69,10 @@ func (st *SSHTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(st.ociStore), ) go func() { - err := st.runtime.RunGadget(st.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := st.runtime.RunGadget(st.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", st.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/symlink.go b/pkg/containerwatcher/v2/tracers/symlink.go index 01aa8cc6e1..8e292aeacd 100644 --- a/pkg/containerwatcher/v2/tracers/symlink.go +++ b/pkg/containerwatcher/v2/tracers/symlink.go @@ -68,7 +68,10 @@ func (st *SymlinkTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(st.ociStore), ) go func() { - err := st.runtime.RunGadget(st.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := st.runtime.RunGadget(st.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", st.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/syscall.go b/pkg/containerwatcher/v2/tracers/syscall.go index cc2e6d8f98..fbbcb13224 100644 --- a/pkg/containerwatcher/v2/tracers/syscall.go +++ b/pkg/containerwatcher/v2/tracers/syscall.go @@ -68,6 +68,7 @@ func (st *SyscallTracer) Start(ctx context.Context) error { params := map[string]string{ "operator.oci.ebpf.map-fetch-count": "0", "operator.oci.ebpf.map-fetch-interval": "30s", + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager } err := st.runtime.RunGadget(st.gadgetCtx, nil, params) if err != nil { diff --git a/pkg/containerwatcher/v2/tracers/unshare.go b/pkg/containerwatcher/v2/tracers/unshare.go index fdf96ac657..d6dd7e61bd 100644 --- a/pkg/containerwatcher/v2/tracers/unshare.go +++ b/pkg/containerwatcher/v2/tracers/unshare.go @@ -68,7 +68,10 @@ func (ut *UnshareTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(ut.ociStore), ) go func() { - err := ut.runtime.RunGadget(ut.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := ut.runtime.RunGadget(ut.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", ut.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/utils/datasource_event.go b/pkg/utils/datasource_event.go index 7317a9717b..fbc57b4305 100644 --- a/pkg/utils/datasource_event.go +++ b/pkg/utils/datasource_event.go @@ -351,6 +351,9 @@ func (e *DatasourceEvent) GetFullPath() string { switch e.EventType { case OpenEventType: path, _ := e.getFieldAccessor("fpath").String(e.Data) + if path == "" { + path, _ = e.getFieldAccessor("fname").String(e.Data) + } return path default: logger.L().Warning("GetFullPath not implemented for event type", helpers.String("eventType", string(e.EventType))) From 248d952ddb3f6134770c28381c47d7dc1833d573 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Thu, 15 Jan 2026 12:05:13 +0100 Subject: [PATCH 14/17] implement review comments Signed-off-by: Matthias Bertschy --- pkg/cloudmetadata/metadata.go | 6 +- .../v2/container_watcher_collection.go | 18 +-- pkg/contextdetection/detectors/host.go | 2 +- pkg/rulemanager/containercallbacks.go | 11 +- pkg/rulemanager/ruleadapters/creator.go | 63 ++++++--- pkg/rulemanager/rulepolicy.go | 12 +- pkg/utils/datasource_event.go | 121 ++++++++++++++---- 7 files changed, 164 insertions(+), 69 deletions(-) diff --git a/pkg/cloudmetadata/metadata.go b/pkg/cloudmetadata/metadata.go index 7d3cee243a..d0cb59a2ac 100644 --- a/pkg/cloudmetadata/metadata.go +++ b/pkg/cloudmetadata/metadata.go @@ -16,6 +16,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ( + azureApiVersion = "2021-12-13" +) + // GetCloudMetadata retrieves cloud metadata for a given node func GetCloudMetadata(ctx context.Context, client *k8sinterface.KubernetesApi, nodeName string) (*apitypes.CloudMetadata, error) { node, err := client.GetKubernetesClient().CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) @@ -207,7 +211,7 @@ func fetchAzureMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { base := "http://169.254.169.254/metadata/instance/compute/" get := func(path string) string { - url := base + path + "?api-version=2021-02-01&format=text" + url := base + path + "?api-version=" + azureApiVersion + "&format=text" req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) req.Header.Set("Metadata", "true") resp, err := client.Do(req) diff --git a/pkg/containerwatcher/v2/container_watcher_collection.go b/pkg/containerwatcher/v2/container_watcher_collection.go index ce7d9f3212..8065a3702e 100644 --- a/pkg/containerwatcher/v2/container_watcher_collection.go +++ b/pkg/containerwatcher/v2/container_watcher_collection.go @@ -5,11 +5,11 @@ import ( "fmt" "time" - eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" - containerutils "github.com/inspektor-gadget/inspektor-gadget/pkg/container-utils" containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" + containerutils "github.com/inspektor-gadget/inspektor-gadget/pkg/container-utils" "github.com/inspektor-gadget/inspektor-gadget/pkg/operators/socketenricher" "github.com/inspektor-gadget/inspektor-gadget/pkg/params" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" "github.com/inspektor-gadget/inspektor-gadget/pkg/utils/host" "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" @@ -108,7 +108,7 @@ func (cw *ContainerWatcher) StartContainerCollection(ctx context.Context) error // Create virtual host container if host monitoring enabled if cw.cfg.HostMonitoringEnabled { - virtualHostContainer, err := getHostAsContainer() + virtualHostContainer, err := GetHostAsContainer() if err != nil { logger.L().Warning("ContainerManager - failed to create virtual host container", helpers.Error(err)) @@ -172,21 +172,21 @@ func (cw *ContainerWatcher) addRunningContainers(notf *rulebindingmanager.RuleBi } } -// getHostAsContainer creates a synthetic container representing the host's mount namespace. +// GetHostAsContainer creates a synthetic container representing the host's mount namespace. // This enables host-level events to be processed through the standard container infrastructure. -func getHostAsContainer() (*containercollection.Container, error) { - pidOne := 1 +func GetHostAsContainer() (*containercollection.Container, error) { + hostInitPID := 1 // Get the mount namespace ID for PID 1 (init process) - mntns, err := containerutils.GetMntNs(pidOne) + mntns, err := containerutils.GetMntNs(hostInitPID) if err != nil { - return nil, fmt.Errorf("getting mount namespace for host PID %d: %w", pidOne, err) + return nil, fmt.Errorf("getting mount namespace for host PID %d: %w", hostInitPID, err) } return &containercollection.Container{ Runtime: containercollection.RuntimeMetadata{ BasicRuntimeMetadata: eventtypes.BasicRuntimeMetadata{ - ContainerPID: uint32(pidOne), + ContainerPID: uint32(hostInitPID), }, }, Mntns: mntns, diff --git a/pkg/contextdetection/detectors/host.go b/pkg/contextdetection/detectors/host.go index 43b9c98093..7cfc3e61bb 100644 --- a/pkg/contextdetection/detectors/host.go +++ b/pkg/contextdetection/detectors/host.go @@ -44,7 +44,7 @@ func (hd *HostDetector) Detect(container *containercollection.Container) (contex return nil, false } - if container.Runtime.ContainerID != "" { + if container.ContainerPid() == 1 { hostInfo := &HostContextInfo{HostName: hd.hostName} return hostInfo, true } diff --git a/pkg/rulemanager/containercallbacks.go b/pkg/rulemanager/containercallbacks.go index 4e12048d0c..f54899ef0f 100644 --- a/pkg/rulemanager/containercallbacks.go +++ b/pkg/rulemanager/containercallbacks.go @@ -10,6 +10,7 @@ import ( containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" + "github.com/kubescape/node-agent/pkg/contextdetection/detectors" "github.com/kubescape/node-agent/pkg/objectcache" "github.com/kubescape/node-agent/pkg/utils" ) @@ -65,10 +66,16 @@ func (rm *RuleManager) ContainerCallback(notif containercollection.PubSubEvent) // Detect context and add to the registry contextInfo, err := rm.detectorManager.DetectContext(notif.Container) if err != nil { - logger.L().Warning("RuleManager - failed to detect context", + logger.L().Warning("RuleManager - failed to detect context, defaulting to standalone", helpers.String("container ID", notif.Container.Runtime.ContainerID), helpers.Error(err)) - } else if contextInfo != nil && notif.Container.Mntns != 0 { + contextInfo = &detectors.StandaloneContextInfo{ + ContainerID: notif.Container.Runtime.ContainerID, + ContainerName: notif.Container.Runtime.ContainerName, + } + } + + if contextInfo != nil && notif.Container.Mntns != 0 { if err := rm.mntnsRegistry.Register(notif.Container.Mntns, contextInfo); err != nil { logger.L().Warning("RuleManager - failed to register in mntns registry", helpers.String("container ID", notif.Container.Runtime.ContainerID), diff --git a/pkg/rulemanager/ruleadapters/creator.go b/pkg/rulemanager/ruleadapters/creator.go index a3fdbc6336..a9c3f904d0 100644 --- a/pkg/rulemanager/ruleadapters/creator.go +++ b/pkg/rulemanager/ruleadapters/creator.go @@ -113,6 +113,11 @@ func (r *RuleFailureCreator) enrichRuleFailure(ruleFailure *types.GenericRuleFai } func (r *RuleFailureCreator) setProfileMetadata(rule typesv1.Rule, ruleFailure *types.GenericRuleFailure, objectCache objectcache.ObjectCache) { + triggerEvent := ruleFailure.GetTriggerEvent() + if triggerEvent == nil { + return + } + var profileType armotypes.ProfileType baseRuntimeAlert := ruleFailure.GetBaseRuntimeAlert() profileRequirment := rule.ProfileDependency @@ -131,7 +136,7 @@ func (r *RuleFailureCreator) setProfileMetadata(rule typesv1.Rule, ruleFailure * switch profileType { case armotypes.ApplicationProfile: - state := objectCache.ApplicationProfileCache().GetApplicationProfileState(ruleFailure.GetTriggerEvent().GetContainerID()) + state := objectCache.ApplicationProfileCache().GetApplicationProfileState(triggerEvent.GetContainerID()) if state != nil { profileMetadata := &armotypes.ProfileMetadata{ Status: state.Status, @@ -148,7 +153,7 @@ func (r *RuleFailureCreator) setProfileMetadata(rule typesv1.Rule, ruleFailure * } case armotypes.NetworkProfile: - state := objectCache.NetworkNeighborhoodCache().GetNetworkNeighborhoodState(ruleFailure.GetTriggerEvent().GetContainerID()) + state := objectCache.NetworkNeighborhoodCache().GetNetworkNeighborhoodState(triggerEvent.GetContainerID()) if state != nil { profileMetadata := &armotypes.ProfileMetadata{ Status: state.Status, @@ -175,7 +180,12 @@ func (r *RuleFailureCreator) setProfileMetadata(rule typesv1.Rule, ruleFailure * } func (r *RuleFailureCreator) setCloudServices(ruleFailure *types.GenericRuleFailure) { - if cloudServices := r.dnsManager.ResolveContainerProcessToCloudServices(ruleFailure.GetTriggerEvent().GetContainerID(), ruleFailure.GetBaseRuntimeAlert().InfectedPID); cloudServices != nil { + triggerEvent := ruleFailure.GetTriggerEvent() + if triggerEvent == nil { + return + } + + if cloudServices := r.dnsManager.ResolveContainerProcessToCloudServices(triggerEvent.GetContainerID(), ruleFailure.GetBaseRuntimeAlert().InfectedPID); cloudServices != nil { ruleFailure.SetCloudServices(cloudServices.ToSlice()) } @@ -186,6 +196,8 @@ func (r *RuleFailureCreator) setBaseRuntimeAlert(ruleFailure *types.GenericRuleF var err error var path string + triggerEvent := ruleFailure.GetTriggerEvent() + if ruleFailure.GetRuntimeProcessDetails().ProcessTree.Path == "" { path, err = utils.GetPathFromPid(ruleFailure.GetRuntimeProcessDetails().ProcessTree.PID) if err != nil { @@ -195,8 +207,8 @@ func (r *RuleFailureCreator) setBaseRuntimeAlert(ruleFailure *types.GenericRuleF } if err != nil { // FIXME WTF it's always nil here - if ruleFailure.GetRuntimeProcessDetails().ProcessTree.Path != "" { - hostPath = filepath.Join("/proc", fmt.Sprintf("/%d/root/%s", r.containerIdToPid.Get(ruleFailure.GetTriggerEvent().GetContainerID()), + if ruleFailure.GetRuntimeProcessDetails().ProcessTree.Path != "" && triggerEvent != nil { + hostPath = filepath.Join("/proc", fmt.Sprintf("/%d/root/%s", r.containerIdToPid.Get(triggerEvent.GetContainerID()), ruleFailure.GetRuntimeProcessDetails().ProcessTree.Path)) } } else { @@ -205,7 +217,9 @@ func (r *RuleFailureCreator) setBaseRuntimeAlert(ruleFailure *types.GenericRuleF baseRuntimeAlert := ruleFailure.GetBaseRuntimeAlert() - baseRuntimeAlert.Timestamp = time.Unix(0, int64(ruleFailure.GetTriggerEvent().GetTimestamp())) + if triggerEvent != nil { + baseRuntimeAlert.Timestamp = time.Unix(0, int64(triggerEvent.GetTimestamp())) + } var size int64 = 0 if hostPath != "" { size, err = utils.GetFileSize(hostPath) @@ -243,36 +257,41 @@ func (r *RuleFailureCreator) setBaseRuntimeAlert(ruleFailure *types.GenericRuleF func (r *RuleFailureCreator) setRuntimeAlertK8sDetails(ruleFailure *types.GenericRuleFailure, objectCache objectcache.ObjectCache) { runtimek8sdetails := ruleFailure.GetRuntimeAlertK8sDetails() + triggerEvent := ruleFailure.GetTriggerEvent() + if triggerEvent == nil { + return + } + if runtimek8sdetails.Image == "" { - runtimek8sdetails.Image = ruleFailure.GetTriggerEvent().GetContainerImage() + runtimek8sdetails.Image = triggerEvent.GetContainerImage() } if runtimek8sdetails.ImageDigest == "" { - runtimek8sdetails.ImageDigest = ruleFailure.GetTriggerEvent().GetContainerImageDigest() + runtimek8sdetails.ImageDigest = triggerEvent.GetContainerImageDigest() } if runtimek8sdetails.Namespace == "" { - runtimek8sdetails.Namespace = ruleFailure.GetTriggerEvent().GetNamespace() + runtimek8sdetails.Namespace = triggerEvent.GetNamespace() } if runtimek8sdetails.PodName == "" { - runtimek8sdetails.PodName = ruleFailure.GetTriggerEvent().GetPod() + runtimek8sdetails.PodName = triggerEvent.GetPod() } if runtimek8sdetails.PodNamespace == "" { - runtimek8sdetails.PodNamespace = ruleFailure.GetTriggerEvent().GetNamespace() + runtimek8sdetails.PodNamespace = triggerEvent.GetNamespace() } if runtimek8sdetails.ContainerName == "" { - runtimek8sdetails.ContainerName = ruleFailure.GetTriggerEvent().GetContainer() + runtimek8sdetails.ContainerName = triggerEvent.GetContainer() } if runtimek8sdetails.ContainerID == "" { - runtimek8sdetails.ContainerID = ruleFailure.GetTriggerEvent().GetContainerID() + runtimek8sdetails.ContainerID = triggerEvent.GetContainerID() } if runtimek8sdetails.HostNetwork == nil { - hostNetwork := ruleFailure.GetTriggerEvent().GetHostNetwork() + hostNetwork := triggerEvent.GetHostNetwork() runtimek8sdetails.HostNetwork = &hostNetwork } @@ -330,12 +349,16 @@ func (r *RuleFailureCreator) setContextSpecificFields(ruleFailure *types.Generic if k8sDetails.ContainerID == "" { k8sDetails.ContainerID = enrichedEvent.ContainerID } - if k8sDetails.Image == "" { - k8sDetails.Image = ruleFailure.GetTriggerEvent().GetContainerImage() - } - // Use container name from trigger event if available - if k8sDetails.ContainerName == "" { - k8sDetails.ContainerName = ruleFailure.GetTriggerEvent().GetContainer() + + triggerEvent := ruleFailure.GetTriggerEvent() + if triggerEvent != nil { + if k8sDetails.Image == "" { + k8sDetails.Image = triggerEvent.GetContainerImage() + } + // Use container name from trigger event if available + if k8sDetails.ContainerName == "" { + k8sDetails.ContainerName = triggerEvent.GetContainer() + } } } diff --git a/pkg/rulemanager/rulepolicy.go b/pkg/rulemanager/rulepolicy.go index ca79b8fcd5..eef804a449 100644 --- a/pkg/rulemanager/rulepolicy.go +++ b/pkg/rulemanager/rulepolicy.go @@ -45,21 +45,15 @@ func RuleAppliesToContext(rule *typesv1.Rule, contextInfo contextdetection.Conte var contextTags []string for _, tag := range rule.Tags { - if strings.HasPrefix(tag, "context:") { - ctx := strings.TrimPrefix(tag, "context:") + if ctx, found := strings.CutPrefix(tag, "context:"); found { contextTags = append(contextTags, ctx) } } if len(contextTags) > 0 { - for _, ctx := range contextTags { - if ctx == string(currentContext) { - return true - } - } - return false + return slices.Contains(contextTags, string(currentContext)) } - // No context specified in either field or tags: default to kubernetes only (backward compatible) + // No context specified in tags: default to kubernetes only (backward compatible) return currentContext == contextdetection.Kubernetes } diff --git a/pkg/utils/datasource_event.go b/pkg/utils/datasource_event.go index fbc57b4305..499298b36d 100644 --- a/pkg/utils/datasource_event.go +++ b/pkg/utils/datasource_event.go @@ -1,6 +1,7 @@ package utils import ( + "errors" "net" "net/http" "os" @@ -10,6 +11,7 @@ import ( igconsts "github.com/inspektor-gadget/inspektor-gadget/gadgets/trace_exec/consts" "github.com/inspektor-gadget/inspektor-gadget/pkg/datasource" + "github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-service/api" "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets" "github.com/inspektor-gadget/inspektor-gadget/pkg/types" "github.com/inspektor-gadget/inspektor-gadget/pkg/utils/syscalls" @@ -81,6 +83,93 @@ var _ SshEvent = (*DatasourceEvent)(nil) var _ SyscallEvent = (*DatasourceEvent)(nil) var _ UnshareEvent = (*DatasourceEvent)(nil) +var errFieldNotFound = errors.New("field not found") + +type nullFieldAccessor struct{} + +func (n *nullFieldAccessor) Name() string { return "" } +func (n *nullFieldAccessor) FullName() string { return "" } +func (n *nullFieldAccessor) Size() uint32 { return 0 } +func (n *nullFieldAccessor) Get(data datasource.Data) []byte { return nil } +func (n *nullFieldAccessor) Set(data datasource.Data, value []byte) error { return nil } +func (n *nullFieldAccessor) IsRequested() bool { return false } +func (n *nullFieldAccessor) AddSubField(name string, kind api.Kind, opts ...datasource.FieldOption) (datasource.FieldAccessor, error) { + return nil, errFieldNotFound +} +func (n *nullFieldAccessor) GetSubFieldsWithTag(tag ...string) []datasource.FieldAccessor { return nil } +func (n *nullFieldAccessor) Parent() datasource.FieldAccessor { return nil } +func (n *nullFieldAccessor) SubFields() []datasource.FieldAccessor { return nil } +func (n *nullFieldAccessor) SetHidden(hidden bool, recurse bool) {} +func (n *nullFieldAccessor) Type() api.Kind { return api.Kind_Invalid } +func (n *nullFieldAccessor) Flags() uint32 { return 0 } +func (n *nullFieldAccessor) Tags() []string { return nil } +func (n *nullFieldAccessor) AddTags(tags ...string) {} +func (n *nullFieldAccessor) HasAllTagsOf(tags ...string) bool { return false } +func (n *nullFieldAccessor) HasAnyTagsOf(tags ...string) bool { return false } +func (n *nullFieldAccessor) Annotations() map[string]string { return nil } +func (n *nullFieldAccessor) AddAnnotation(key, value string) {} +func (n *nullFieldAccessor) RemoveReference(recurse bool) {} +func (n *nullFieldAccessor) Rename(string) error { return errFieldNotFound } + +func (n *nullFieldAccessor) Uint8(datasource.Data) (uint8, error) { return 0, errFieldNotFound } +func (n *nullFieldAccessor) Uint16(datasource.Data) (uint16, error) { return 0, errFieldNotFound } +func (n *nullFieldAccessor) Uint32(datasource.Data) (uint32, error) { return 0, errFieldNotFound } +func (n *nullFieldAccessor) Uint64(datasource.Data) (uint64, error) { return 0, errFieldNotFound } +func (n *nullFieldAccessor) Int8(datasource.Data) (int8, error) { return 0, errFieldNotFound } +func (n *nullFieldAccessor) Int16(datasource.Data) (int16, error) { return 0, errFieldNotFound } +func (n *nullFieldAccessor) Int32(datasource.Data) (int32, error) { return 0, errFieldNotFound } +func (n *nullFieldAccessor) Int64(datasource.Data) (int64, error) { return 0, errFieldNotFound } +func (n *nullFieldAccessor) Float32(datasource.Data) (float32, error) { return 0, errFieldNotFound } +func (n *nullFieldAccessor) Float64(datasource.Data) (float64, error) { return 0, errFieldNotFound } +func (n *nullFieldAccessor) String(datasource.Data) (string, error) { return "", errFieldNotFound } +func (n *nullFieldAccessor) Bytes(datasource.Data) ([]byte, error) { return nil, errFieldNotFound } +func (n *nullFieldAccessor) Bool(datasource.Data) (bool, error) { return false, errFieldNotFound } + +func (n *nullFieldAccessor) Uint8Array(datasource.Data) ([]uint8, error) { + return nil, errFieldNotFound +} +func (n *nullFieldAccessor) Uint16Array(datasource.Data) ([]uint16, error) { + return nil, errFieldNotFound +} +func (n *nullFieldAccessor) Uint32Array(datasource.Data) ([]uint32, error) { + return nil, errFieldNotFound +} +func (n *nullFieldAccessor) Uint64Array(datasource.Data) ([]uint64, error) { + return nil, errFieldNotFound +} +func (n *nullFieldAccessor) Int8Array(datasource.Data) ([]int8, error) { return nil, errFieldNotFound } +func (n *nullFieldAccessor) Int16Array(datasource.Data) ([]int16, error) { + return nil, errFieldNotFound +} +func (n *nullFieldAccessor) Int32Array(datasource.Data) ([]int32, error) { + return nil, errFieldNotFound +} +func (n *nullFieldAccessor) Int64Array(datasource.Data) ([]int64, error) { + return nil, errFieldNotFound +} +func (n *nullFieldAccessor) Float32Array(datasource.Data) ([]float32, error) { + return nil, errFieldNotFound +} +func (n *nullFieldAccessor) Float64Array(datasource.Data) ([]float64, error) { + return nil, errFieldNotFound +} + +func (n *nullFieldAccessor) PutUint8(datasource.Data, uint8) error { return errFieldNotFound } +func (n *nullFieldAccessor) PutUint16(datasource.Data, uint16) error { return errFieldNotFound } +func (n *nullFieldAccessor) PutUint32(datasource.Data, uint32) error { return errFieldNotFound } +func (n *nullFieldAccessor) PutUint64(datasource.Data, uint64) error { return errFieldNotFound } +func (n *nullFieldAccessor) PutInt8(datasource.Data, int8) error { return errFieldNotFound } +func (n *nullFieldAccessor) PutInt16(datasource.Data, int16) error { return errFieldNotFound } +func (n *nullFieldAccessor) PutInt32(datasource.Data, int32) error { return errFieldNotFound } +func (n *nullFieldAccessor) PutInt64(datasource.Data, int64) error { return errFieldNotFound } +func (n *nullFieldAccessor) PutFloat32(datasource.Data, float32) error { return errFieldNotFound } +func (n *nullFieldAccessor) PutFloat64(datasource.Data, float64) error { return errFieldNotFound } +func (n *nullFieldAccessor) PutString(datasource.Data, string) error { return errFieldNotFound } +func (n *nullFieldAccessor) PutBytes(datasource.Data, []byte) error { return errFieldNotFound } +func (n *nullFieldAccessor) PutBool(datasource.Data, bool) error { return errFieldNotFound } + +var missingFieldAccessor datasource.FieldAccessor = &nullFieldAccessor{} + func (e *DatasourceEvent) getFieldAccessor(fieldName string) datasource.FieldAccessor { cache, loaded := fieldCaches.Load(e.EventType) if !loaded { @@ -91,9 +180,9 @@ func (e *DatasourceEvent) getFieldAccessor(fieldName string) datasource.FieldAcc field := e.Datasource.GetField(fieldName) accessor, _ = cache.(*sync.Map).LoadOrStore(fieldName, field) } - // Handle case where field doesn't exist (returns nil) + // Handle case where field doesn't exist if accessor == nil { - return nil + return missingFieldAccessor } return accessor.(datasource.FieldAccessor) } @@ -171,16 +260,7 @@ func (e *DatasourceEvent) GetComm() string { container := e.GetContainer() return container default: - comm := e.getFieldAccessor("proc.comm") - if comm == nil { - logger.L().Warning("GetComm - proc.comm field not found in event type", helpers.String("eventType", string(e.EventType))) - return "" - } - commValue, err := comm.String(e.Data) - if err != nil { - logger.L().Warning("GetComm - cannot read proc.comm field in event", helpers.String("eventType", string(e.EventType))) - return "" - } + commValue, _ := e.getFieldAccessor("proc.comm").String(e.Data) return commValue } } @@ -408,11 +488,7 @@ func (e *DatasourceEvent) GetModule() string { } func (e *DatasourceEvent) GetMountNsID() uint64 { - accessor := e.getFieldAccessor("proc.mntns_id") - if accessor == nil { - return 0 - } - mountNsID, _ := accessor.Uint64(e.Data) + mountNsID, _ := e.getFieldAccessor("proc.mntns_id").Uint64(e.Data) return mountNsID } @@ -510,16 +586,7 @@ func (e *DatasourceEvent) GetPID() uint32 { containerPid, _ := e.getFieldAccessor("runtime.containerPid").Uint32(e.Data) return containerPid default: - pid := e.getFieldAccessor("proc.pid") - if pid == nil { - logger.L().Warning("GetPID - proc.pid field not found in event type", helpers.String("eventType", string(e.EventType))) - return 0 - } - pidValue, err := pid.Uint32(e.Data) - if err != nil { - logger.L().Warning("GetPID cannot read proc.pid field in event", helpers.String("eventType", string(e.EventType))) - return 0 - } + pidValue, _ := e.getFieldAccessor("proc.pid").Uint32(e.Data) return pidValue } } From 81372009cb3878c0d33efc6430a223a5449d5ee4 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Thu, 15 Jan 2026 12:54:58 +0100 Subject: [PATCH 15/17] refactor: enhance cloud metadata retrieval with improved fallback strategy and timeout settings Signed-off-by: Matthias Bertschy --- pkg/cloudmetadata/metadata.go | 190 +++++++++++++++------------------- 1 file changed, 86 insertions(+), 104 deletions(-) diff --git a/pkg/cloudmetadata/metadata.go b/pkg/cloudmetadata/metadata.go index d0cb59a2ac..c752632a95 100644 --- a/pkg/cloudmetadata/metadata.go +++ b/pkg/cloudmetadata/metadata.go @@ -18,6 +18,7 @@ import ( const ( azureApiVersion = "2021-12-13" + metadataTimeout = 2 * time.Second ) // GetCloudMetadata retrieves cloud metadata for a given node @@ -61,169 +62,149 @@ func GetCloudMetadataWithIMDS(ctx context.Context) (*apitypes.CloudMetadata, err cMetadataClient := k8sInterfaceCloudMetadata.NewMetadataClient(true) cMetadata, err := cMetadataClient.GetMetadata(ctx) - if err != nil { - logger.L().Info("failed to get cloud metadata from IMDS, trying fallbacks", helpers.Error(err)) + if err == nil { + return cMetadata, nil + } - // Try DigitalOcean metadata endpoints as a fallback (e.g., droplets) if IMDS didn't work. - if doMeta, derr := fetchDigitalOceanMetadata(ctx); derr == nil && doMeta != nil { - logger.L().Info("retrieved cloud metadata from DigitalOcean metadata service") - return doMeta, nil - } + logger.L().Info("failed to get cloud metadata from IMDS, trying fallbacks", helpers.Error(err)) - // Try GCP metadata endpoints as a fallback - if gcpMeta, gerr := fetchGCPMetadata(ctx); gerr == nil && gcpMeta != nil { - logger.L().Info("retrieved cloud metadata from GCP metadata service") - return gcpMeta, nil - } + // Fallback strategy: try different providers + fallbacks := []struct { + name string + fetch func(context.Context) (*apitypes.CloudMetadata, error) + }{ + {name: "DigitalOcean", fetch: fetchDigitalOceanMetadata}, + {name: "GCP", fetch: fetchGCPMetadata}, + {name: "Azure", fetch: fetchAzureMetadata}, + } - // Try Azure metadata endpoints as a fallback - if azureMeta, aerr := fetchAzureMetadata(ctx); aerr == nil && azureMeta != nil { - logger.L().Info("retrieved cloud metadata from Azure metadata service") - return azureMeta, nil + for _, fb := range fallbacks { + if meta, ferr := fb.fetch(ctx); ferr == nil && meta != nil { + logger.L().Info(fmt.Sprintf("retrieved cloud metadata from %s metadata service", fb.name)) + return meta, nil } + } - // Wrap the underlying error with additional context so logs make it clearer why metadata is missing. - // This helps surface issues like IMDS token endpoint failures (e.g. IMDSv2 token 404), unreachable metadata endpoints, - // or provider-specific metadata problems. - return nil, fmt.Errorf("failed to get cloud metadata from IMDS: %w", err) + // Wrap the underlying error with additional context so logs make it clearer why metadata is missing. + return nil, fmt.Errorf("failed to get cloud metadata from IMDS or fallbacks: %w", err) +} + +// fetchHTTPMetadata helper to fetch metadata from a URL with optional headers +func fetchHTTPMetadata(ctx context.Context, url string, headers map[string]string) (string, error) { + client := &http.Client{ + Timeout: metadataTimeout, + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", err + } + for k, v := range headers { + req.Header.Set(k, v) } + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("metadata endpoint %s returned status: %d", url, resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return strings.TrimSpace(string(body)), nil +} - return cMetadata, nil +func getLastPathPart(val string) string { + if val == "" { + return "" + } + parts := strings.Split(val, "/") + return parts[len(parts)-1] } // fetchDigitalOceanMetadata attempts to fetch basic metadata from DigitalOcean's metadata service. -// -// It probes the metadata root and queries a few commonly available endpoints. -// It returns a non-nil error if it does not look like DigitalOcean's metadata service or no useful values were found. func fetchDigitalOceanMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { - client := &http.Client{ - Timeout: 2 * time.Second, - } base := "http://169.254.169.254/metadata/v1/" // Probe root to see whether the metadata endpoint responds and contains expected entries. - req, _ := http.NewRequestWithContext(ctx, http.MethodGet, base, nil) - resp, err := client.Do(req) + body, err := fetchHTTPMetadata(ctx, base, nil) if err != nil { return nil, err } - defer resp.Body.Close() - if resp.StatusCode != 200 { - return nil, fmt.Errorf("digitalocean metadata root returned status: %d", resp.StatusCode) - } - body, _ := io.ReadAll(resp.Body) - bstr := string(body) + // Basic heuristic: the DO metadata root typically lists resources like 'id', 'hostname', 'region' etc. - if !strings.Contains(bstr, "id") && !strings.Contains(bstr, "region") && !strings.Contains(bstr, "hostname") { + if !strings.Contains(body, "id") && !strings.Contains(body, "region") && !strings.Contains(body, "hostname") { return nil, fmt.Errorf("digitalocean metadata root missing expected entries") } - // helper to fetch a single textual endpoint and return trimmed result or empty string get := func(path string) string { - url := base + path - r, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - resp2, err2 := client.Do(r) - if err2 != nil || resp2.StatusCode != 200 { - if resp2 != nil { - resp2.Body.Close() - } - return "" - } - defer resp2.Body.Close() - b, _ := io.ReadAll(resp2.Body) - return strings.TrimSpace(string(b)) + val, _ := fetchHTTPMetadata(ctx, base+path, nil) + return val } id := get("id") if id == "" { id = get("droplet_id") } - hostname := get("hostname") - region := get("region") instanceType := get("size") if instanceType == "" { instanceType = get("type") } - privateIP := get("interfaces/private/0/ipv4/address") - publicIP := get("interfaces/public/0/ipv4/address") + + meta := &apitypes.CloudMetadata{ + Provider: "digitalocean", + InstanceID: id, + InstanceType: instanceType, + Region: get("region"), + PrivateIP: get("interfaces/private/0/ipv4/address"), + PublicIP: get("interfaces/public/0/ipv4/address"), + Hostname: get("hostname"), + } // if nothing useful was obtained, return an error so callers can continue trying other fallbacks - if id == "" && hostname == "" && region == "" && privateIP == "" && publicIP == "" && instanceType == "" { + if meta.InstanceID == "" && meta.Hostname == "" && meta.Region == "" && meta.PrivateIP == "" && meta.PublicIP == "" && meta.InstanceType == "" { return nil, fmt.Errorf("digitalocean metadata endpoints returned no data") } - return &apitypes.CloudMetadata{ - Provider: "digitalocean", - InstanceID: id, - InstanceType: instanceType, - Region: region, - PrivateIP: privateIP, - PublicIP: publicIP, - Hostname: hostname, - }, nil + return meta, nil } // fetchGCPMetadata attempts to fetch basic metadata from GCP's metadata service. func fetchGCPMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { - client := &http.Client{ - Timeout: 2 * time.Second, - } - base := "http://metadata.google.internal/computeMetadata/v1/instance/" + base := "http://metadata.google.internal/computeMetadata/v1/" + headers := map[string]string{"Metadata-Flavor": "Google"} get := func(path string) string { - req, _ := http.NewRequestWithContext(ctx, http.MethodGet, base+path, nil) - req.Header.Set("Metadata-Flavor", "Google") - resp, err := client.Do(req) - if err != nil || resp.StatusCode != 200 { - if resp != nil { - resp.Body.Close() - } - return "" - } - defer resp.Body.Close() - b, _ := io.ReadAll(resp.Body) - return strings.TrimSpace(string(b)) + val, _ := fetchHTTPMetadata(ctx, base+path, headers) + return val } - machineType := get("machine-type") + machineType := get("instance/machine-type") if machineType == "" { return nil, fmt.Errorf("not a GCP instance") } - // GCP returns full path like "projects/12345/machineTypes/n1-standard-1" - parts := strings.Split(machineType, "/") - instanceType := parts[len(parts)-1] - return &apitypes.CloudMetadata{ Provider: "gcp", - InstanceID: get("id"), - InstanceType: instanceType, - Zone: get("zone"), - Hostname: get("hostname"), + AccountID: get("project/project-id"), + InstanceID: get("instance/id"), + InstanceType: getLastPathPart(machineType), + Zone: getLastPathPart(get("instance/zone")), + Hostname: get("instance/hostname"), }, nil } // fetchAzureMetadata attempts to fetch basic metadata from Azure's metadata service. func fetchAzureMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { - client := &http.Client{ - Timeout: 2 * time.Second, - } base := "http://169.254.169.254/metadata/instance/compute/" + headers := map[string]string{"Metadata": "true"} + params := "?api-version=" + azureApiVersion + "&format=text" get := func(path string) string { - url := base + path + "?api-version=" + azureApiVersion + "&format=text" - req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - req.Header.Set("Metadata", "true") - resp, err := client.Do(req) - if err != nil || resp.StatusCode != 200 { - if resp != nil { - resp.Body.Close() - } - return "" - } - defer resp.Body.Close() - b, _ := io.ReadAll(resp.Body) - return strings.TrimSpace(string(b)) + val, _ := fetchHTTPMetadata(ctx, base+path+params, headers) + return val } vmSize := get("vmSize") @@ -233,6 +214,7 @@ func fetchAzureMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { return &apitypes.CloudMetadata{ Provider: "azure", + AccountID: get("subscriptionId"), InstanceID: get("vmId"), InstanceType: vmSize, Region: get("location"), From 3787d23dadcd83a4f95be836e51cdf443bc116a9 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Thu, 15 Jan 2026 15:31:48 +0200 Subject: [PATCH 16/17] change to use the Registry interface Signed-off-by: Yakir Oren --- pkg/contextdetection/detectors/manager.go | 4 ++-- pkg/rulemanager/rule_manager.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/contextdetection/detectors/manager.go b/pkg/contextdetection/detectors/manager.go index 6701410dd7..5c01f2f4b8 100644 --- a/pkg/contextdetection/detectors/manager.go +++ b/pkg/contextdetection/detectors/manager.go @@ -14,13 +14,13 @@ var ( ) type DetectorManager struct { - registry *contextdetection.MntnsRegistry + registry contextdetection.Registry detectors []contextdetection.ContextDetector } // NewDetectorManager creates a new DetectorManager with the given registry. // Detectors are initialized in priority order: K8s (0), Host (1), Standalone (2). -func NewDetectorManager(registry *contextdetection.MntnsRegistry) *DetectorManager { +func NewDetectorManager(registry contextdetection.Registry) *DetectorManager { dm := &DetectorManager{ registry: registry, detectors: []contextdetection.ContextDetector{ diff --git a/pkg/rulemanager/rule_manager.go b/pkg/rulemanager/rule_manager.go index 5b3840fe5a..408631e5a5 100644 --- a/pkg/rulemanager/rule_manager.go +++ b/pkg/rulemanager/rule_manager.go @@ -59,7 +59,7 @@ type RuleManager struct { adapterFactory *ruleadapters.EventRuleAdapterFactory ruleFailureCreator ruleadapters.RuleFailureCreatorInterface rulePolicyValidator *RulePolicyValidator - mntnsRegistry *contextdetection.MntnsRegistry + mntnsRegistry contextdetection.Registry detectorManager *detectors.DetectorManager } @@ -79,7 +79,7 @@ func CreateRuleManager( ruleCooldown *rulecooldown.RuleCooldown, adapterFactory *ruleadapters.EventRuleAdapterFactory, celEvaluator cel.CELRuleEvaluator, - mntnsRegistry *contextdetection.MntnsRegistry, + mntnsRegistry contextdetection.Registry, ) (*RuleManager, error) { ruleFailureCreator := ruleadapters.NewRuleFailureCreator(enricher, dnsManager, adapterFactory) rulePolicyValidator := NewRulePolicyValidator(objectCache) From f483f66d77168f1f74ec199a50b813b63a138ccc Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Thu, 15 Jan 2026 14:48:55 +0100 Subject: [PATCH 17/17] add design document for multi-context rule engine Signed-off-by: Matthias Bertschy --- docs/RULE_ENGINE_MULTI_CONTEXT_REDESIGN.md | 148 +++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/RULE_ENGINE_MULTI_CONTEXT_REDESIGN.md diff --git a/docs/RULE_ENGINE_MULTI_CONTEXT_REDESIGN.md b/docs/RULE_ENGINE_MULTI_CONTEXT_REDESIGN.md new file mode 100644 index 0000000000..d027fc11b4 --- /dev/null +++ b/docs/RULE_ENGINE_MULTI_CONTEXT_REDESIGN.md @@ -0,0 +1,148 @@ +# Rule Engine Multi-Context Redesign + +## Overview + +This document describes the design and implementation of the multi-context rule engine in the Kubescape Node Agent. The system enables runtime security monitoring and alerting across three distinct execution contexts: + +1. **Kubernetes**: Containers running within a Kubernetes cluster (Pod-based). +2. **Host**: The underlying node itself, treated as a specialized context for monitoring host-level activities. +3. **Standalone**: Non-Kubernetes containers (e.g., Docker containers, standalone containerd instances) that are not managed by the Kubernetes orchestrator. + +## Goals + +- Provide a unified rule evaluation engine for all execution contexts. +- Use the mount namespace (mntns) ID as the primary key for identifying event contexts. +- Support multiple container runtimes through automated discovery (fanotify). +- Allow fine-grained control over where rules apply using context-specific tags. +- Maintain backward compatibility with existing Kubernetes-only monitoring and alert formats. + +## Architecture + +### 1. Event Source Context + +The system defines three primary context types in `pkg/contextdetection/types.go`: + +```go +type EventSourceContext string + +const ( + Kubernetes EventSourceContext = "kubernetes" + Host EventSourceContext = "host" + Standalone EventSourceContext = "standalone" +) +``` + +### 2. Context Detection and Registry + +The architecture relies on a discovery mechanism that identifies the nature of a process or container when it starts. + +#### Context Info and Detectors +Each detected context is represented by a `ContextInfo` object which provides the context type and a unique `WorkloadID`. + +```go +type ContextInfo interface { + Context() EventSourceContext + WorkloadID() string +} +``` + +The `DetectorManager` coordinates several `ContextDetector` implementations: +- **K8sDetector**: Identifies containers enriched with Kubernetes metadata (Namespace, Pod name). +- **HostDetector**: Identifies the host context based on PID 1 or the host's mount namespace. +- **StandaloneDetector**: Identifies containers that have runtime information but lack Kubernetes metadata. + +#### Mount Namespace Registry +The `MntnsRegistry` maintains a thread-safe mapping of mount namespace IDs to their corresponding `ContextInfo`. This registry is the "source of truth" used to enrich eBPF events as they arrive. + +```go +type Registry interface { + Register(mntns uint64, info ContextInfo) error + Lookup(mntns uint64) (ContextInfo, bool) + Unregister(mntns uint64) +} +``` + +### 3. Event Enrichment + +As eBPF events (exec, open, network, etc.) are captured, they are wrapped in an `EnrichedEvent`. The `RuleManager` enriches these events with context information by looking up the event's mount namespace ID in the registry. + +```go +func (rm *RuleManager) enrichEventWithContext(enrichedEvent *events.EnrichedEvent) { + mntnsID := enrichedEvent.Event.GetMountNsID() + enrichedEvent.MountNamespaceID = mntnsID + + if mntnsID != 0 { + if contextInfo, found := rm.mntnsRegistry.Lookup(mntnsID); found { + enrichedEvent.SourceContext = contextInfo + } + } +} +``` + +### 4. Rule Evaluation Logic + +#### Context-Aware Filtering +Rules can specify where they should execute using the `context:` tag prefix. The `RuleAppliesToContext` function determines if a rule is applicable: + +- If a rule has tags like `context:host`, it will only run for events detected as `Host`. +- If a rule has no `context:` tags, it defaults to `Kubernetes` only, ensuring backward compatibility for existing rule sets. + +```go +func RuleAppliesToContext(rule *typesv1.Rule, contextInfo contextdetection.ContextInfo) bool { + // ... logic to check "context:" tags ... + // Default: return currentContext == contextdetection.Kubernetes +} +``` + +#### Profile Dependencies +Kubernetes-specific features like Application Profiles and Network Neighborhoods are only enforced for the `Kubernetes` context. Rules requiring these profiles are skipped for `Host` and `Standalone` contexts. + +### 5. Alert Structure + +The `GenericRuleFailure` structure has been extended to include the `SourceContext`. To maintain compatibility with existing consumers (like the Kubescape Cloud or third-party SIEMs), context-specific metadata is mapped into the existing `RuntimeAlertK8sDetails` structure where appropriate: + +- **Host alerts**: The node's hostname is populated in the `NodeName` field. +- **Standalone alerts**: Container ID and Image information are populated, while K8s-specific fields (Namespace, Pod) remain empty. + +```go +type GenericRuleFailure struct { + // ... existing fields ... + SourceContext contextdetection.EventSourceContext +} +``` + +### 6. Multiple Runtime Discovery + +The Node Agent leverages `inspektor-gadget`'s `WithContainerFanotifyEbpf()` capability. This allows the agent to: +1. Use fanotify to watch for OCI runtime (runc, crun) executions. +2. Capture the container's bundle directory and PID. +3. Automatically detect and monitor containers regardless of whether they were started by `kubelet`, `docker`, or `containerd` directly. + +## Configuration + +Context monitoring is configurable via the Node Agent configuration: + +```yaml +# Enable/disable specific monitoring contexts +hostMonitoringEnabled: true +standaloneMonitoringEnabled: true + +# Note: Kubernetes monitoring is usually tied to enableRuntimeDetection +enableRuntimeDetection: true +``` + +## Implementation Status + +- [x] **Core Infrastructure**: Definition of context types and the `MntnsRegistry`. +- [x] **Detector Framework**: Implementation of K8s, Host, and Standalone detectors. +- [x] **Event Enrichment**: Integration into `RuleManager` to attach context to every event. +- [x] **Context-Aware Rules**: Support for `context:` tags in rule definitions. +- [x] **Unified Alerting**: Updated `RuleFailureCreator` to handle multi-context metadata. +- [x] **Multi-Runtime Support**: Integration with fanotify for standalone container discovery. +- [x] **Testing**: Unit and integration tests for context detection and rule application. + +## Future Considerations + +- **Standalone Profiles**: Extending Application Profile learning to standalone containers. +- **Host Policy**: Specific rule sets tailored for host-level hardening and monitoring. +- **Dynamic Context Tags**: Allowing users to define custom contexts based on container labels or environment variables. \ No newline at end of file