diff --git a/pkg/security/metrics/metrics.go b/pkg/security/metrics/metrics.go index afb31ebf31de..42b201a0d77f 100644 --- a/pkg/security/metrics/metrics.go +++ b/pkg/security/metrics/metrics.go @@ -87,6 +87,18 @@ var ( // Tags: - MetricDentryERPCResolutionTimeUs = newRuntimeMetric(".dentry_resolver.erpc_avg_resolution_time_usec") + // String interner metrics + + // MetricStringInternerHits is the counter of hits in a string interner + // Tags: interner + MetricStringInternerHits = newRuntimeMetric(".string_interner.hits") + // MetricStringInternerMisses is the counter of misses in a string interner + // Tags: interner + MetricStringInternerMisses = newRuntimeMetric(".string_interner.misses") + // MetricStringInternerSize is the current size of a string interner + // Tags: interner + MetricStringInternerSize = newRuntimeMetric(".string_interner.size") + // DNS Resolver metrics // MetricDNSResolverIPResolverCache is the counter for the IP resolver (A and AAAA records) diff --git a/pkg/security/probe/fuzz_test.go b/pkg/security/probe/fuzz_test.go index 0b3c0dd87f79..0e80445b0c70 100644 --- a/pkg/security/probe/fuzz_test.go +++ b/pkg/security/probe/fuzz_test.go @@ -31,6 +31,7 @@ import ( "github.com/DataDog/datadog-agent/pkg/security/resolvers/usergroup" "github.com/DataDog/datadog-agent/pkg/security/secl/model" securityprofile "github.com/DataDog/datadog-agent/pkg/security/security_profile" + "github.com/DataDog/datadog-agent/pkg/security/utils" "github.com/DataDog/datadog-agent/pkg/util/ktime" ddsync "github.com/DataDog/datadog-agent/pkg/util/sync" ) @@ -104,7 +105,7 @@ func newFuzzEBPFProbe(tb testing.TB) *EBPFProbe { tb.Fatalf("failed to create test process resolver: %v", err) } - dentryResolver, err := dentry.NewResolver(probeConfig, noopSD, nil) + dentryResolver, err := dentry.NewResolver(probeConfig, noopSD, nil, utils.NewLRUStringInterner(16384, "basename")) if err != nil { tb.Fatalf("failed to create dentry resolver: %v", err) } diff --git a/pkg/security/probe/probe_ebpf.go b/pkg/security/probe/probe_ebpf.go index 35cb7e6a392e..f33f8fc695ee 100644 --- a/pkg/security/probe/probe_ebpf.go +++ b/pkg/security/probe/probe_ebpf.go @@ -1010,6 +1010,17 @@ func (p *EBPFProbe) SendStats() error { } } + if p.Resolvers.BasenameInterner != nil { + if err := p.Resolvers.BasenameInterner.SendStats(p.statsdClient, metrics.MetricStringInternerHits, metrics.MetricStringInternerMisses, metrics.MetricStringInternerSize); err != nil { + return err + } + } + if p.Resolvers.PathInterner != nil { + if err := p.Resolvers.PathInterner.SendStats(p.statsdClient, metrics.MetricStringInternerHits, metrics.MetricStringInternerMisses, metrics.MetricStringInternerSize); err != nil { + return err + } + } + value := p.BPFFilterTruncated.Swap(0) if err := p.statsdClient.Count(metrics.MetricBPFFilterTruncated, int64(value), []string{}, 1.0); err != nil { return err @@ -3143,7 +3154,13 @@ func NewEBPFProbe(probe *Probe, config *config.Config, hostname string, opts Opt WorkloadMeta: opts.WorkloadMeta, } - p.Resolvers, err = resolvers.NewEBPFResolvers(config, p.Manager, probe.StatsdClient, probe.scrubber, p.Erpc, resolversOpts) + // String interners. Basenames are shared between the dentry resolver and the activity tree + // FileNode.Name. Full paths are stored only on activity tree FileNode leaves and have a much + // higher cardinality, so they get their own larger LRU to avoid evicting basenames. + basenameInterner := utils.NewLRUStringInterner(16384, "basename") + pathInterner := utils.NewLRUStringInterner(65536, "path") + + p.Resolvers, err = resolvers.NewEBPFResolvers(config, p.Manager, probe.StatsdClient, probe.scrubber, p.Erpc, resolversOpts, basenameInterner, pathInterner) if err != nil { return nil, err } diff --git a/pkg/security/resolvers/dentry/resolver.go b/pkg/security/resolvers/dentry/resolver.go index f215acd507ed..07f4985bde40 100644 --- a/pkg/security/resolvers/dentry/resolver.go +++ b/pkg/security/resolvers/dentry/resolver.go @@ -76,6 +76,8 @@ type Resolver struct { erpcResolutionAvgTime float64 erpcResolutionNumValues int64 erpcResolutionTimesLock sync.Mutex + + interner *utils.LRUStringInterner } // ErrEntryNotFound is thrown when a path key was not found in the cache @@ -464,7 +466,7 @@ func (dr *Resolver) cacheEntries(keys []model.PathKey, names [][]byte) error { for i, k := range keys { cacheEntry := PathEntry{ - Name: string(names[i]), + Name: dr.interner.Deduplicate(string(names[i])), } if len(keys) > i+1 { cacheEntry.Parent = keys[i+1] @@ -763,7 +765,7 @@ func (dr *Resolver) Close() error { } // NewResolver returns a new dentry resolver -func NewResolver(config *config.Config, statsdClient statsd.ClientInterface, e *erpc.ERPC) (*Resolver, error) { +func NewResolver(config *config.Config, statsdClient statsd.ClientInterface, e *erpc.ERPC, interner *utils.LRUStringInterner) (*Resolver, error) { hitsCounters := make(map[counterEntry]*atomic.Int64) missCounters := make(map[counterEntry]*atomic.Int64) for _, resolution := range metrics.AllResolutionsTags { @@ -802,5 +804,6 @@ func NewResolver(config *config.Config, statsdClient statsd.ClientInterface, e * missCounters: missCounters, numCPU: numCPU, challenge: rand.Uint32(), + interner: interner, }, nil } diff --git a/pkg/security/resolvers/process/resolver_ebpf.go b/pkg/security/resolvers/process/resolver_ebpf.go index b86a37df9b15..6d229b24f4fc 100644 --- a/pkg/security/resolvers/process/resolver_ebpf.go +++ b/pkg/security/resolvers/process/resolver_ebpf.go @@ -495,7 +495,7 @@ type argsEnvsCacheEntry struct { truncated bool } -var argsEnvsInterner = utils.NewLRUStringInterner(argsEnvsValueCacheSize) +var argsEnvsInterner = utils.NewLRUStringInterner(argsEnvsValueCacheSize, "args_envs") func parseStringArray(data []byte) ([]string, bool) { truncated := false diff --git a/pkg/security/resolvers/resolvers_ebpf.go b/pkg/security/resolvers/resolvers_ebpf.go index b1eb6b1d7f55..80bee7ac91b8 100644 --- a/pkg/security/resolvers/resolvers_ebpf.go +++ b/pkg/security/resolvers/resolvers_ebpf.go @@ -66,12 +66,18 @@ type EBPFResolvers struct { FileMetadataResolver *file.Resolver SignatureResolver *sign.Resolver + // BasenameInterner deduplicates path components (basenames). Shared between the dentry resolver + // cache entries and the activity tree FileNode.Name field. + BasenameInterner *utils.LRUStringInterner + // PathInterner deduplicates full reduced file paths stored on activity tree FileNode leaves. + PathInterner *utils.LRUStringInterner + SnapshotUsingListmount bool } // NewEBPFResolvers creates a new instance of EBPFResolvers -func NewEBPFResolvers(config *config.Config, manager *manager.Manager, statsdClient statsd.ClientInterface, scrubber *utils.Scrubber, eRPC *erpc.ERPC, opts Opts) (*EBPFResolvers, error) { - dentryResolver, err := dentry.NewResolver(config.Probe, statsdClient, eRPC) +func NewEBPFResolvers(config *config.Config, manager *manager.Manager, statsdClient statsd.ClientInterface, scrubber *utils.Scrubber, eRPC *erpc.ERPC, opts Opts, basenameInterner, pathInterner *utils.LRUStringInterner) (*EBPFResolvers, error) { + dentryResolver, err := dentry.NewResolver(config.Probe, statsdClient, eRPC, basenameInterner) if err != nil { return nil, err } @@ -218,6 +224,8 @@ func NewEBPFResolvers(config *config.Config, manager *manager.Manager, statsdCli FileMetadataResolver: fileMetadataResolver, SnapshotUsingListmount: config.Probe.SnapshotUsingListmount, SignatureResolver: sign.NewSignatureResolver(), + BasenameInterner: basenameInterner, + PathInterner: pathInterner, } return resolvers, nil diff --git a/pkg/security/security_profile/activity_tree/activity_tree.go b/pkg/security/security_profile/activity_tree/activity_tree.go index 3387be1d4f2f..832486ae3cc5 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree.go +++ b/pkg/security/security_profile/activity_tree/activity_tree.go @@ -170,6 +170,15 @@ type ActivityTree struct { // top level lists used to summarize the content of the tree DNSNames *utils.StringKeys SyscallsMask map[int]int + + imageTagIDs []imageTagEntry +} + +// imageTagEntry is a slot in the ActivityTree's image tag registry. +// inUse is false for tombstoned slots that are free for reuse. +type imageTagEntry struct { + tag string + inUse bool } // CookieToProcessNodeCacheSize defines the "cookie to process" node cache size @@ -190,6 +199,70 @@ func NewActivityTree(validator Owner, pathsReducer *PathsReducer, treeType strin } } +// GetImageTagID returns the internal ID for an image tag, or 0 if not found. +// Returns 0 for an empty tag (0 is the null sentinel). +func (at *ActivityTree) GetImageTagID(imageTag string) uint64 { + if imageTag == "" { + return 0 + } + for i, entry := range at.imageTagIDs { + if entry.inUse && entry.tag == imageTag { + return uint64(i) + 1 + } + } + return 0 +} + +// removeImageTag marks the slot for this image tag as free for reuse +func (at *ActivityTree) removeImageTag(imageTag string) uint64 { + for i, entry := range at.imageTagIDs { + if entry.inUse && entry.tag == imageTag { + at.imageTagIDs[i].inUse = false + return uint64(i + 1) + } + } + return 0 +} + +// GetOrInsertImageTag returns the internal ID for an image tag, creating one if it doesn't exist. +// Returns 0 for an empty tag, which is treated as a no-op by all callers (0 is the null sentinel). +func (at *ActivityTree) GetOrInsertImageTag(imageTag string) uint64 { + if imageTag == "" { + return 0 + } + + firstFree := uint64(0) + + for i, entry := range at.imageTagIDs { + if entry.inUse && entry.tag == imageTag { + return uint64(i) + 1 + } + if firstFree == 0 && !entry.inUse { + firstFree = uint64(i) + 1 + } + } + + if firstFree > 0 { + at.imageTagIDs[firstFree-1] = imageTagEntry{tag: imageTag, inUse: true} + return firstFree + } + + at.imageTagIDs = append(at.imageTagIDs, imageTagEntry{tag: imageTag, inUse: true}) + return uint64(len(at.imageTagIDs)) +} + +// GetTagFromID returns the image tag string for a given internal ID, or "" if the ID is invalid or the slot is free. +func (at *ActivityTree) GetTagFromID(imageTagID uint64) string { + if imageTagID == 0 || imageTagID > uint64(len(at.imageTagIDs)) { + return "" + } + entry := at.imageTagIDs[imageTagID-1] + if !entry.inUse { + return "" + } + return entry.tag +} + // SetType changes the type and owner of the ActivityTree func (at *ActivityTree) SetType(treeType string, validator Owner) { at.treeType = treeType @@ -212,8 +285,8 @@ func (at *ActivityTree) AppendChild(node *ProcessNode) { node.Parent = at } -// AppendImageTag appends the given image tag -func (at *ActivityTree) AppendImageTag(_ string, _ time.Time) { +// AppendImageTagID appends the given image tag +func (at *ActivityTree) AppendImageTagID(_ uint64, _ time.Time) { } // GetParent returns nil for the ActivityTree @@ -340,22 +413,37 @@ func (at *ActivityTree) isEventValid(event *model.Event, dryRun bool) (bool, err // Insert inserts the event in the activity tree func (at *ActivityTree) Insert(event *model.Event, insertMissingProcesses bool, imageTag string, generationType NodeGenerationType, resolvers *resolvers.EBPFResolvers) (bool, error) { - newEntry, err := at.insertEvent(event, false /* !dryRun */, insertMissingProcesses, imageTag, generationType, resolvers) + freshTag := at.GetImageTagID(imageTag) == 0 + imageTagID := at.GetOrInsertImageTag(imageTag) + + newEntry, err := at.insertEvent(event, false /* !dryRun */, insertMissingProcesses, imageTagID, generationType, resolvers) if newEntry { // this doesn't count the exec events which are counted separately at.Stats.counts[event.GetEventType()].addedCount[generationType].Inc() } + + // If we just registered a brand-new tag but the event was rejected before any node + // could reference it, free the slot immediately so it doesn't linger indefinitely. + if freshTag && err != nil { + at.removeImageTag(imageTag) + } + return newEntry, err } // Contains looks up the event in the activity tree func (at *ActivityTree) Contains(event *model.Event, insertMissingProcesses bool, imageTag string, generationType NodeGenerationType, resolvers *resolvers.EBPFResolvers) (bool, error) { - newEntry, err := at.insertEvent(event, true /* dryRun */, insertMissingProcesses, imageTag, generationType, resolvers) + imageTagID := at.GetImageTagID(imageTag) + if imageTagID == 0 { + return false, nil + } + + newEntry, err := at.insertEvent(event, true /* dryRun */, insertMissingProcesses, imageTagID, generationType, resolvers) return !newEntry, err } // insert inserts the event in the activity tree, returns true if the event generated a new entry in the tree -func (at *ActivityTree) insertEvent(event *model.Event, dryRun bool, insertMissingProcesses bool, imageTag string, generationType NodeGenerationType, resolvers *resolvers.EBPFResolvers) (bool, error) { +func (at *ActivityTree) insertEvent(event *model.Event, dryRun bool, insertMissingProcesses bool, imageTagID uint64, generationType NodeGenerationType, resolvers *resolvers.EBPFResolvers) (bool, error) { // sanity check if generationType == Unknown || generationType > MaxNodeGenerationType { return false, fmt.Errorf("invalid generation type: %v", generationType) @@ -367,7 +455,7 @@ func (at *ActivityTree) insertEvent(event *model.Event, dryRun bool, insertMissi } // Next we'll call CreateProcessNode, which will retrieve the process node if already present, or create a new one (with all its lineage if needed). - node, newProcessNode, err := at.CreateProcessNode(event.ProcessCacheEntry, imageTag, generationType, !insertMissingProcesses /*dryRun*/, resolvers) + node, newProcessNode, err := at.createProcessNode(event.ProcessCacheEntry, imageTagID, generationType, !insertMissingProcesses /*dryRun*/, resolvers) if err != nil { return false, err } @@ -391,7 +479,7 @@ func (at *ActivityTree) insertEvent(event *model.Event, dryRun bool, insertMissi // the count of processed events is the count of events that matched the activity dump selector = the events for // which we successfully found a process activity node at.Stats.counts[event.GetEventType()].processedCount.Inc() - node.AppendImageTag(imageTag, event.ResolveEventTime()) + node.AppendImageTagID(imageTagID, event.ResolveEventTime()) // insert the event based on its type switch event.GetEventType() { case model.ExecEventType: @@ -399,19 +487,19 @@ func (at *ActivityTree) insertEvent(event *model.Event, dryRun bool, insertMissi node.MatchedRules = model.AppendMatchedRule(node.MatchedRules, event.Rules) return newProcessNode, nil case model.FileOpenEventType: - return node.InsertFileEvent(&event.Open.File, event, imageTag, generationType, at.Stats, dryRun, at.pathsReducer, resolvers), nil + return node.InsertFileEvent(&event.Open.File, event, imageTagID, generationType, at.Stats, dryRun, at.pathsReducer, resolvers), nil case model.DNSEventType: - return node.InsertDNSEvent(event, imageTag, generationType, at.Stats, at.DNSNames, dryRun, at.DNSMatchMaxDepth), nil + return node.InsertDNSEvent(event, imageTagID, generationType, at.Stats, at.DNSNames, dryRun, at.DNSMatchMaxDepth), nil case model.IMDSEventType: - return node.InsertIMDSEvent(event, imageTag, generationType, at.Stats, dryRun), nil + return node.InsertIMDSEvent(event, imageTagID, generationType, at.Stats, dryRun), nil case model.BindEventType: - return node.InsertBindEvent(event, imageTag, generationType, at.Stats, dryRun), nil + return node.InsertBindEvent(event, imageTagID, generationType, at.Stats, dryRun), nil case model.SyscallsEventType: - return node.InsertSyscalls(event, imageTag, at.SyscallsMask, at.Stats, dryRun), nil + return node.InsertSyscalls(event, imageTagID, at.SyscallsMask, at.Stats, dryRun), nil case model.NetworkFlowMonitorEventType: - return node.InsertNetworkFlowMonitorEvent(event, imageTag, generationType, at.Stats, dryRun), nil + return node.InsertNetworkFlowMonitorEvent(event, imageTagID, generationType, at.Stats, dryRun), nil case model.CapabilitiesEventType: - return node.InsertCapabilitiesUsageEvent(event, imageTag, at.Stats, dryRun), nil + return node.InsertCapabilitiesUsageEvent(event, imageTagID, at.Stats, dryRun), nil case model.ExitEventType: // Update the exit time of the process (this is purely informative, do not rely on timestamps to detect // execed children) @@ -480,7 +568,7 @@ func GetNextAncestorBinaryOrArgv0(entry *model.ProcessContext) *model.ProcessCac // - check if one of the ancestors of entry is already in the tree and has a shortcut thanks to its cookie // - creates the list of ancestors "we care about" for the tree, i.e. the chain of ancestors created by calling // "GetNextAncestorBinaryOrArgv0" and that match the tree selector. -func (at *ActivityTree) buildBranchAndLookupCookies(entry *model.ProcessCacheEntry, imageTag string) ([]*model.ProcessCacheEntry, *ProcessNode, error) { +func (at *ActivityTree) buildBranchAndLookupCookies(entry *model.ProcessCacheEntry, imageTagID uint64) ([]*model.ProcessCacheEntry, *ProcessNode, error) { var cs cookieSelector var fastMatch *ProcessNode var found bool @@ -493,7 +581,7 @@ func (at *ActivityTree) buildBranchAndLookupCookies(entry *model.ProcessCacheEnt if cs.isSet() { fastMatch, found = at.CookieToProcessNode.Get(cs) if found { - fastMatch.applyImageTagOnLineageIfNeeded(imageTag) + fastMatch.applyImageTagOnLineageIfNeeded(imageTagID) return branch, fastMatch, nil } } @@ -523,8 +611,19 @@ func (at *ActivityTree) buildBranchAndLookupCookies(entry *model.ProcessCacheEnt return branch, nil, ErrNotValidRootNode } -// CreateProcessNode looks up or inserts the provided entry in the tree func (at *ActivityTree) CreateProcessNode(entry *model.ProcessCacheEntry, imageTag string, generationType NodeGenerationType, dryRun bool, resolvers *resolvers.EBPFResolvers) (*ProcessNode, bool, error) { + var imageTagID uint64 + if dryRun { + // dry-run must not mutate the registry; if the tag is unknown it stays unknown (ID 0) + imageTagID = at.GetImageTagID(imageTag) + } else { + imageTagID = at.GetOrInsertImageTag(imageTag) + } + return at.createProcessNode(entry, imageTagID, generationType, dryRun, resolvers) +} + +// createProcessNode looks up or inserts the provided entry in the tree +func (at *ActivityTree) createProcessNode(entry *model.ProcessCacheEntry, imageTagID uint64, generationType NodeGenerationType, dryRun bool, resolvers *resolvers.EBPFResolvers) (*ProcessNode, bool, error) { if entry == nil { return nil, false, nil } @@ -539,7 +638,7 @@ func (at *ActivityTree) CreateProcessNode(entry *model.ProcessCacheEntry, imageT // Check if entry or one of its parents cookies are in CookieToProcessNode while building the branch we're trying to // insert. - branchToInsert, quickMatch, err := at.buildBranchAndLookupCookies(entry, imageTag) + branchToInsert, quickMatch, err := at.buildBranchAndLookupCookies(entry, imageTagID) if err != nil { return nil, false, err } @@ -559,10 +658,10 @@ func (at *ActivityTree) CreateProcessNode(entry *model.ProcessCacheEntry, imageT parent = quickMatch } - return at.insertBranch(parent, branchToInsert, imageTag, generationType, dryRun, resolvers) + return at.insertBranch(parent, branchToInsert, imageTagID, generationType, dryRun, resolvers) } -func (at *ActivityTree) insertBranch(parent ProcessNodeParent, branchToInsert []*model.ProcessCacheEntry, imageTag string, generationType NodeGenerationType, dryRun bool, r *resolvers.EBPFResolvers) (*ProcessNode, bool, error) { +func (at *ActivityTree) insertBranch(parent ProcessNodeParent, branchToInsert []*model.ProcessCacheEntry, imageTagID uint64, generationType NodeGenerationType, dryRun bool, r *resolvers.EBPFResolvers) (*ProcessNode, bool, error) { var matchingNode *ProcessNode var branchIncrement int var newNode, newNodeFromRebase bool @@ -603,13 +702,13 @@ func (at *ActivityTree) insertBranch(parent ProcessNodeParent, branchToInsert [] } // if we reach this point, we can safely return the last inserted entry and indicate that the tree was modified - matchingNode.applyImageTagOnLineageIfNeeded(imageTag) + matchingNode.applyImageTagOnLineageIfNeeded(imageTagID) return matchingNode, true, nil } // if we reach this point, we've successfully found the matching node in the tree without modifying the tree if matchingNode != nil { - matchingNode.applyImageTagOnLineageIfNeeded(imageTag) + matchingNode.applyImageTagOnLineageIfNeeded(imageTagID) } return matchingNode, newNode, nil } @@ -704,7 +803,7 @@ func (at *ActivityTree) rebaseTree(parent ProcessNodeParent, childIndexToRebase // ChildNodeToRebase and topLevelNode match and need to be merged, rebase the one in the profile, and insert // the remaining nodes of the branch on top of it newRebasedChild := at.rebaseTree(parent, childIndexToRebase, newParent, nil, generationType, resolvers) - output, _, _ := at.insertBranch(newRebasedChild, branchToInsert[:len(branchToInsert)-1], "", generationType, false, resolvers) + output, _, _ := at.insertBranch(newRebasedChild, branchToInsert[:len(branchToInsert)-1], 0, generationType, false, resolvers) if output == nil { return newRebasedChild @@ -843,8 +942,9 @@ func (at *ActivityTree) SendStats(client statsd.ClientInterface) error { // TagAllNodes tags all the activity tree's nodes with the given image tag func (at *ActivityTree) TagAllNodes(imageTag string, timestamp time.Time) { + imageTagID := at.GetOrInsertImageTag(imageTag) for _, rootNode := range at.ProcessNodes { - rootNode.TagAllNodes(imageTag, timestamp) + rootNode.TagAllNodes(imageTagID, timestamp) } } @@ -853,16 +953,22 @@ func (at *ActivityTree) EvictImageTag(imageTag string) { // purge the cookies which todays are never set. TODO: once they'll get used, recompute them here at.CookieToProcessNode.Purge() + imageTagID := at.GetImageTagID(imageTag) + if imageTagID == 0 { + return + } + // recompute also the full list of DNSNames and Syscalls when evicting nodes DNSNames := utils.NewStringKeys(nil) SyscallsMask := make(map[int]int) newProcessNodes := []*ProcessNode{} for _, node := range at.ProcessNodes { - if shouldRemoveNode := node.EvictImageTag(imageTag, DNSNames, SyscallsMask); !shouldRemoveNode { + if shouldRemoveNode := node.EvictImageTag(imageTagID, DNSNames, SyscallsMask); !shouldRemoveNode { newProcessNodes = append(newProcessNodes, node) } } at.ProcessNodes = newProcessNodes + at.removeImageTag(imageTag) } func (at *ActivityTree) visitProcessNode(processNode *ProcessNode, cb func(processNode *ProcessNode)) { @@ -968,6 +1074,11 @@ type ImageProcessKey struct { func (at *ActivityTree) EvictUnusedNodes(before time.Time, filepathsInProcessCache map[ImageProcessKey]bool, profileImageName, profileImageTag string) int { totalEvicted := 0 + // Resolve once here so every node uses the same ID without querying the registry repeatedly. + // If the tag is not registered (e.g. no events have been seen for it), ID is 0 and the + // process-cache protection path is a no-op — consistent with nodes having no entry for that tag. + profileImageTagID := at.GetImageTagID(profileImageTag) + // Iterate through all process nodes and evict unused nodes for i := len(at.ProcessNodes) - 1; i >= 0; i-- { node := at.ProcessNodes[i] @@ -976,11 +1087,11 @@ func (at *ActivityTree) EvictUnusedNodes(before time.Time, filepathsInProcessCac } // Evict unused nodes - evicted := node.EvictUnusedNodes(before, filepathsInProcessCache, profileImageName, profileImageTag) + evicted := node.EvictUnusedNodes(before, filepathsInProcessCache, profileImageName, profileImageTag, profileImageTagID) totalEvicted += evicted // If the process node itself has no image tags left after eviction, remove it entirely - if len(node.Seen) == 0 { + if node.SeenIsEmpty() { // Remove the node at.ProcessNodes = append(at.ProcessNodes[:i], at.ProcessNodes[i+1:]...) totalEvicted++ diff --git a/pkg/security/security_profile/activity_tree/activity_tree_proto_dec_v1.go b/pkg/security/security_profile/activity_tree/activity_tree_proto_dec_v1.go index 0effd09f451f..a67548dbfe49 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree_proto_dec_v1.go +++ b/pkg/security/security_profile/activity_tree/activity_tree_proto_dec_v1.go @@ -20,12 +20,15 @@ import ( // ProtoDecodeActivityTree decodes an ActivityTree structure func ProtoDecodeActivityTree(dest *ActivityTree, nodes []*adproto.ProcessActivityNode) { + getIDFromTag := func(imageTag string) uint64 { + return dest.GetOrInsertImageTag(imageTag) + } for _, node := range nodes { - dest.ProcessNodes = append(dest.ProcessNodes, protoDecodeProcessActivityNode(dest, node)) + dest.ProcessNodes = append(dest.ProcessNodes, protoDecodeProcessActivityNode(dest, node, getIDFromTag)) } } -func protoDecodeProcessActivityNode(parent ProcessNodeParent, pan *adproto.ProcessActivityNode) *ProcessNode { +func protoDecodeProcessActivityNode(parent ProcessNodeParent, pan *adproto.ProcessActivityNode, getIDFromImageTag func(string) uint64) *ProcessNode { if pan == nil { return nil } @@ -50,7 +53,7 @@ func protoDecodeProcessActivityNode(parent ProcessNodeParent, pan *adproto.Proce for tag, imageTagTimes := range pan.NodeBase.Seen { firstSeen := ProtoDecodeTimestamp(imageTagTimes.FirstSeen) lastSeen := ProtoDecodeTimestamp(imageTagTimes.LastSeen) - ppan.RecordWithTimestamps(tag, firstSeen, lastSeen) + ppan.RecordWithTimestamps(getIDFromImageTag(tag), firstSeen, lastSeen) } } @@ -59,16 +62,16 @@ func protoDecodeProcessActivityNode(parent ProcessNodeParent, pan *adproto.Proce } for _, child := range pan.Children { - ppan.Children = append(ppan.Children, protoDecodeProcessActivityNode(ppan, child)) + ppan.Children = append(ppan.Children, protoDecodeProcessActivityNode(ppan, child, getIDFromImageTag)) } for _, fan := range pan.Files { - protoDecodedFan := protoDecodeFileActivityNode(fan) + protoDecodedFan := protoDecodeFileActivityNode(fan, getIDFromImageTag) ppan.Files[protoDecodedFan.Name] = protoDecodedFan } for _, dns := range pan.DnsNames { - protoDecodedDNS := protoDecodeDNSNode(dns) + protoDecodedDNS := protoDecodeDNSNode(dns, getIDFromImageTag) if len(protoDecodedDNS.Requests) != 0 { name := protoDecodedDNS.Requests[0].Question.Name ppan.DNSNames[name] = protoDecodedDNS @@ -76,16 +79,16 @@ func protoDecodeProcessActivityNode(parent ProcessNodeParent, pan *adproto.Proce } for _, imds := range pan.ImdsEvents { - node := protoDecodeIMDSNode(imds) + node := protoDecodeIMDSNode(imds, getIDFromImageTag) ppan.IMDSEvents[node.Event] = node } for _, socket := range pan.Sockets { - ppan.Sockets = append(ppan.Sockets, protoDecodeProtoSocket(socket)) + ppan.Sockets = append(ppan.Sockets, protoDecodeProtoSocket(socket, getIDFromImageTag)) } for _, sysc := range pan.SyscallNodes { - ppan.Syscalls = append(ppan.Syscalls, protoDecodeSyscallNode(sysc)) + ppan.Syscalls = append(ppan.Syscalls, protoDecodeSyscallNode(sysc, getIDFromImageTag)) } for _, networkDevice := range pan.NetworkDevices { @@ -93,17 +96,17 @@ func protoDecodeProcessActivityNode(parent ProcessNodeParent, pan *adproto.Proce NetNS: networkDevice.Netns, IfIndex: networkDevice.Ifindex, IfName: networkDevice.Ifname, - }] = protoDecodeNetworkDevice(networkDevice) + }] = protoDecodeNetworkDevice(networkDevice, getIDFromImageTag) } for _, capNode := range pan.CapabilityNodes { - ppan.Capabilities = append(ppan.Capabilities, decodeProtoCapabilityNode(capNode)) + ppan.Capabilities = append(ppan.Capabilities, decodeProtoCapabilityNode(capNode, getIDFromImageTag)) } return ppan } -func protoDecodeSyscallNode(sysc *adproto.SyscallNode) *SyscallNode { +func protoDecodeSyscallNode(sysc *adproto.SyscallNode, getIDFromImageTag func(imageTag string) uint64) *SyscallNode { if sysc == nil { return nil } @@ -118,7 +121,7 @@ func protoDecodeSyscallNode(sysc *adproto.SyscallNode) *SyscallNode { for tag, imageTagTimes := range sysc.NodeBase.Seen { firstSeen := ProtoDecodeTimestamp(imageTagTimes.FirstSeen) lastSeen := ProtoDecodeTimestamp(imageTagTimes.LastSeen) - syscallNode.RecordWithTimestamps(tag, firstSeen, lastSeen) + syscallNode.RecordWithTimestamps(getIDFromImageTag(tag), firstSeen, lastSeen) } } @@ -223,6 +226,44 @@ func protoDecodeFileEvent(fi *adproto.FileInfo) *model.FileEvent { return fe } +func protoDecodeStoredFileEvent(fi *adproto.FileInfo) *storedFileEvent { + if fi == nil { + return nil + } + + fe := &storedFileEvent{ + FileFields: model.FileFields{ + UID: fi.Uid, + User: fi.User, + GID: fi.Gid, + Group: fi.Group, + Mode: uint16(fi.Mode), + CTime: fi.Ctime, + MTime: fi.Mtime, + PathKey: model.PathKey{ + MountID: fi.MountId, + Inode: fi.Inode, + }, + InUpperLayer: fi.InUpperLayer, + }, + PathnameStr: fi.Path, + BasenameStr: fi.Basename, + Filesystem: fi.Filesystem, + PkgName: fi.PackageName, + PkgVersion: fi.PackageVersion, + PkgEpoch: int(ptrOrZero(fi.PackageEpoch)), + PkgRelease: ptrOrZero(fi.PackageRelease), + PkgSrcVersion: fi.PackageSrcVersion, + PkgSrcEpoch: int(ptrOrZero(fi.PackageSrcEpoch)), + PkgSrcRelease: ptrOrZero(fi.PackageSrcRelease), + Hashes: make([]string, len(fi.Hashes)), + HashState: model.HashState(fi.HashState), + } + copy(fe.Hashes, fi.Hashes) + + return fe +} + func ptrOrZero[T any](ptr *T) T { if ptr != nil { return *ptr @@ -231,7 +272,7 @@ func ptrOrZero[T any](ptr *T) T { return zero } -func protoDecodeFileActivityNode(fan *adproto.FileActivityNode) *FileNode { +func protoDecodeFileActivityNode(fan *adproto.FileActivityNode, getIDFromImageTag func(string) uint64) *FileNode { if fan == nil { return nil } @@ -239,10 +280,10 @@ func protoDecodeFileActivityNode(fan *adproto.FileActivityNode) *FileNode { pfan := &FileNode{ MatchedRules: make([]*model.MatchedRule, 0, len(fan.MatchedRules)), Name: fan.Name, - File: protoDecodeFileEvent(fan.File), + File: protoDecodeStoredFileEvent(fan.File), GenerationType: NodeGenerationType(fan.GenerationType), Open: protoDecodeOpenNode(fan.Open), - Children: make(map[string]*FileNode, len(fan.Children)), + Children: nil, NodeBase: NewNodeBase(), } @@ -250,7 +291,7 @@ func protoDecodeFileActivityNode(fan *adproto.FileActivityNode) *FileNode { for tag, imageTagTimes := range fan.NodeBase.Seen { firstSeen := ProtoDecodeTimestamp(imageTagTimes.FirstSeen) lastSeen := ProtoDecodeTimestamp(imageTagTimes.LastSeen) - pfan.RecordWithTimestamps(tag, firstSeen, lastSeen) + pfan.RecordWithTimestamps(getIDFromImageTag(tag), firstSeen, lastSeen) } } @@ -258,9 +299,12 @@ func protoDecodeFileActivityNode(fan *adproto.FileActivityNode) *FileNode { pfan.MatchedRules = append(pfan.MatchedRules, protoDecodeProtoMatchedRule(rule)) } - for _, child := range fan.Children { - node := protoDecodeFileActivityNode(child) - pfan.Children[node.Name] = node + if len(fan.Children) > 0 { + pfan.Children = make(map[string]*FileNode, len(fan.Children)) + for _, child := range fan.Children { + node := protoDecodeFileActivityNode(child, getIDFromImageTag) + pfan.Children[node.Name] = node + } } return pfan @@ -282,7 +326,7 @@ func protoDecodeOpenNode(openNode *adproto.OpenNode) *OpenNode { return pon } -func protoDecodeDNSNode(dn *adproto.DNSNode) *DNSNode { +func protoDecodeDNSNode(dn *adproto.DNSNode, getIDFromImageTag func(string) uint64) *DNSNode { if dn == nil { return nil } @@ -297,7 +341,7 @@ func protoDecodeDNSNode(dn *adproto.DNSNode) *DNSNode { for tag, imageTagTimes := range dn.NodeBase.Seen { firstSeen := ProtoDecodeTimestamp(imageTagTimes.FirstSeen) lastSeen := ProtoDecodeTimestamp(imageTagTimes.LastSeen) - pdn.RecordWithTimestamps(tag, firstSeen, lastSeen) + pdn.RecordWithTimestamps(getIDFromImageTag(tag), firstSeen, lastSeen) } } @@ -312,7 +356,7 @@ func protoDecodeDNSNode(dn *adproto.DNSNode) *DNSNode { return pdn } -func protoDecodeNetworkDevice(device *adproto.NetworkDeviceNode) *NetworkDeviceNode { +func protoDecodeNetworkDevice(device *adproto.NetworkDeviceNode, getIDFromImageTag func(string) uint64) *NetworkDeviceNode { if device == nil { return nil } @@ -343,7 +387,7 @@ func protoDecodeNetworkDevice(device *adproto.NetworkDeviceNode) *NetworkDeviceN for tag, imageTagTimes := range flow.NodeBase.Seen { firstSeen := ProtoDecodeTimestamp(imageTagTimes.FirstSeen) lastSeen := ProtoDecodeTimestamp(imageTagTimes.LastSeen) - fn.RecordWithTimestamps(tag, firstSeen, lastSeen) + fn.RecordWithTimestamps(getIDFromImageTag(tag), firstSeen, lastSeen) } } ndn.FlowNodes[f.GetFiveTuple()] = fn @@ -380,7 +424,7 @@ func protoDecodeNetworkStats(stats *adproto.NetworkStats) model.NetworkStats { return ns } -func protoDecodeIMDSNode(in *adproto.IMDSNode) *IMDSNode { +func protoDecodeIMDSNode(in *adproto.IMDSNode, getIDFromImageTag func(string) uint64) *IMDSNode { if in == nil { return nil } @@ -395,7 +439,7 @@ func protoDecodeIMDSNode(in *adproto.IMDSNode) *IMDSNode { for tag, imageTagTimes := range in.NodeBase.Seen { firstSeen := ProtoDecodeTimestamp(imageTagTimes.FirstSeen) lastSeen := ProtoDecodeTimestamp(imageTagTimes.LastSeen) - node.RecordWithTimestamps(tag, firstSeen, lastSeen) + node.RecordWithTimestamps(getIDFromImageTag(tag), firstSeen, lastSeen) } } @@ -466,7 +510,7 @@ func protoDecodeAWSSecurityCredentials(creds *adproto.AWSSecurityCredentials) mo } } -func protoDecodeProtoSocket(sn *adproto.SocketNode) *SocketNode { +func protoDecodeProtoSocket(sn *adproto.SocketNode, getIDFromImageTag func(string) uint64) *SocketNode { if sn == nil { return nil } @@ -489,7 +533,7 @@ func protoDecodeProtoSocket(sn *adproto.SocketNode) *SocketNode { for tag, imageTagTimes := range bindNode.NodeBase.Seen { firstSeen := ProtoDecodeTimestamp(imageTagTimes.FirstSeen) lastSeen := ProtoDecodeTimestamp(imageTagTimes.LastSeen) - psn.RecordWithTimestamps(tag, firstSeen, lastSeen) + psn.RecordWithTimestamps(getIDFromImageTag(tag), firstSeen, lastSeen) } } @@ -524,7 +568,7 @@ func ProtoDecodeTimestamp(nanos uint64) time.Time { return time.Unix(0, int64(nanos)) } -func decodeProtoCapabilityNode(pan *adproto.CapabilityNode) *CapabilityNode { +func decodeProtoCapabilityNode(pan *adproto.CapabilityNode, getIDFromImageTag func(string) uint64) *CapabilityNode { if pan == nil { return nil } @@ -540,7 +584,7 @@ func decodeProtoCapabilityNode(pan *adproto.CapabilityNode) *CapabilityNode { for tag, imageTagTimes := range pan.NodeBase.Seen { firstSeen := ProtoDecodeTimestamp(imageTagTimes.FirstSeen) lastSeen := ProtoDecodeTimestamp(imageTagTimes.LastSeen) - capNode.RecordWithTimestamps(tag, firstSeen, lastSeen) + capNode.RecordWithTimestamps(getIDFromImageTag(tag), firstSeen, lastSeen) } } diff --git a/pkg/security/security_profile/activity_tree/activity_tree_proto_enc_v1.go b/pkg/security/security_profile/activity_tree/activity_tree_proto_enc_v1.go index ffacdc2e9866..bb300e735849 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree_proto_enc_v1.go +++ b/pkg/security/security_profile/activity_tree/activity_tree_proto_enc_v1.go @@ -24,12 +24,12 @@ func ToProto(at *ActivityTree) []*adproto.ProcessActivityNode { out := make([]*adproto.ProcessActivityNode, 0, len(at.ProcessNodes)) for _, node := range at.ProcessNodes { - out = append(out, processActivityNodeToProto(node)) + out = append(out, processActivityNodeToProto(node, at.GetTagFromID)) } return out } -func processActivityNodeToProto(pan *ProcessNode) *adproto.ProcessActivityNode { +func processActivityNodeToProto(pan *ProcessNode, tagIDToImageTag func(id uint64) string) *adproto.ProcessActivityNode { if pan == nil { return nil } @@ -44,7 +44,7 @@ func processActivityNodeToProto(pan *ProcessNode) *adproto.ProcessActivityNode { DnsNames: make([]*adproto.DNSNode, 0, len(pan.DNSNames)), ImdsEvents: make([]*adproto.IMDSNode, 0, len(pan.IMDSEvents)), Sockets: make([]*adproto.SocketNode, 0, len(pan.Sockets)), - NodeBase: nodeBaseToProto(&pan.NodeBase), + NodeBase: nodeBaseToProto(&pan.NodeBase, tagIDToImageTag), SyscallNodes: make([]*adproto.SyscallNode, 0, len(pan.Syscalls)), NetworkDevices: make([]*adproto.NetworkDeviceNode, 0, len(pan.NetworkDevices)), CapabilityNodes: make([]*adproto.CapabilityNode, 0, len(pan.Capabilities)), @@ -55,41 +55,41 @@ func processActivityNodeToProto(pan *ProcessNode) *adproto.ProcessActivityNode { } for _, child := range pan.Children { - ppan.Children = append(ppan.Children, processActivityNodeToProto(child)) + ppan.Children = append(ppan.Children, processActivityNodeToProto(child, tagIDToImageTag)) } for _, fan := range pan.Files { - ppan.Files = append(ppan.Files, fileActivityNodeToProto(fan)) + ppan.Files = append(ppan.Files, fileActivityNodeToProto(fan, tagIDToImageTag)) } for _, dns := range pan.DNSNames { - ppan.DnsNames = append(ppan.DnsNames, dnsNodeToProto(dns)) + ppan.DnsNames = append(ppan.DnsNames, dnsNodeToProto(dns, tagIDToImageTag)) } for _, imds := range pan.IMDSEvents { - ppan.ImdsEvents = append(ppan.ImdsEvents, imdsNodeToProto(imds)) + ppan.ImdsEvents = append(ppan.ImdsEvents, imdsNodeToProto(imds, tagIDToImageTag)) } for _, socket := range pan.Sockets { - ppan.Sockets = append(ppan.Sockets, socketNodeToProto(socket)) + ppan.Sockets = append(ppan.Sockets, socketNodeToProto(socket, tagIDToImageTag)) } for _, sysc := range pan.Syscalls { - ppan.SyscallNodes = append(ppan.SyscallNodes, syscallNodeToProto(sysc)) + ppan.SyscallNodes = append(ppan.SyscallNodes, syscallNodeToProto(sysc, tagIDToImageTag)) } for _, networkDevice := range pan.NetworkDevices { - ppan.NetworkDevices = append(ppan.NetworkDevices, networkDeviceToProto(networkDevice)) + ppan.NetworkDevices = append(ppan.NetworkDevices, networkDeviceToProto(networkDevice, tagIDToImageTag)) } for _, capNode := range pan.Capabilities { - ppan.CapabilityNodes = append(ppan.CapabilityNodes, capabilityNodeToProto(capNode)) + ppan.CapabilityNodes = append(ppan.CapabilityNodes, capabilityNodeToProto(capNode, tagIDToImageTag)) } return ppan } -func networkDeviceToProto(device *NetworkDeviceNode) *adproto.NetworkDeviceNode { +func networkDeviceToProto(device *NetworkDeviceNode, tagIDToImageTag func(id uint64) string) *adproto.NetworkDeviceNode { if device == nil { return nil } @@ -107,15 +107,15 @@ func networkDeviceToProto(device *NetworkDeviceNode) *adproto.NetworkDeviceNode } for _, flowNode := range device.FlowNodes { - ndn.FlowNodes = append(ndn.FlowNodes, flowNodeToProto(flowNode.Flow, &flowNode.NodeBase)) + ndn.FlowNodes = append(ndn.FlowNodes, flowNodeToProto(flowNode.Flow, &flowNode.NodeBase, tagIDToImageTag)) } return ndn } -func flowNodeToProto(flow model.Flow, nodeBase *NodeBase) *adproto.FlowNode { +func flowNodeToProto(flow model.Flow, nodeBase *NodeBase, tagIDToImageTag func(id uint64) string) *adproto.FlowNode { return &adproto.FlowNode{ - NodeBase: nodeBaseToProto(nodeBase), + NodeBase: nodeBaseToProto(nodeBase, tagIDToImageTag), L3Protocol: uint32(flow.L3Protocol), L4Protocol: uint32(flow.L4Protocol), Source: ipPortContextToProto(&flow.Source), @@ -145,13 +145,13 @@ func networkStatsToProto(stats *model.NetworkStats) *adproto.NetworkStats { } } -func syscallNodeToProto(sysc *SyscallNode) *adproto.SyscallNode { +func syscallNodeToProto(sysc *SyscallNode, tagIDToImageTag func(id uint64) string) *adproto.SyscallNode { if sysc == nil { return nil } return &adproto.SyscallNode{ - NodeBase: nodeBaseToProto(&sysc.NodeBase), + NodeBase: nodeBaseToProto(&sysc.NodeBase, tagIDToImageTag), Syscall: int32(sysc.Syscall), } } @@ -250,7 +250,42 @@ func fileEventToProto(fe *model.FileEvent) *adproto.FileInfo { return fi } -func fileActivityNodeToProto(fan *FileNode) *adproto.FileActivityNode { +func storedFileEventToProto(fe *storedFileEvent) *adproto.FileInfo { + if fe == nil { + return nil + } + + fi := adproto.FileInfoFromVTPool() + *fi = adproto.FileInfo{ + Uid: fe.UID, + User: fe.User, + Gid: fe.GID, + Group: fe.Group, + Mode: uint32(fe.Mode), // yeah sorry + Ctime: fe.CTime, + Mtime: fe.MTime, + MountId: fe.MountID, + Inode: fe.Inode, + InUpperLayer: fe.InUpperLayer, + Path: escape(fe.PathnameStr), + Basename: escape(fe.BasenameStr), + Filesystem: escape(fe.Filesystem), + PackageName: fe.PkgName, + PackageVersion: fe.PkgVersion, + PackageEpoch: pointer.Ptr(uint32(fe.PkgEpoch)), + PackageRelease: pointer.Ptr(fe.PkgRelease), + PackageSrcVersion: fe.PkgSrcVersion, + PackageSrcEpoch: pointer.Ptr(uint32(fe.PkgSrcEpoch)), + PackageSrcRelease: pointer.Ptr(fe.PkgSrcRelease), + Hashes: make([]string, len(fe.Hashes)), + HashState: adproto.HashState(fe.HashState), + } + copy(fi.Hashes, fe.Hashes) + + return fi +} + +func fileActivityNodeToProto(fan *FileNode, tagIDToImageTag func(id uint64) string) *adproto.FileActivityNode { if fan == nil { return nil } @@ -259,11 +294,11 @@ func fileActivityNodeToProto(fan *FileNode) *adproto.FileActivityNode { *pfan = adproto.FileActivityNode{ MatchedRules: make([]*adproto.MatchedRule, 0, len(fan.MatchedRules)), Name: escape(fan.Name), - File: fileEventToProto(fan.File), + File: storedFileEventToProto(fan.File), GenerationType: adproto.GenerationType(fan.GenerationType), Open: openNodeToProto(fan.Open), Children: make([]*adproto.FileActivityNode, 0, len(fan.Children)), - NodeBase: nodeBaseToProto(&fan.NodeBase), + NodeBase: nodeBaseToProto(&fan.NodeBase, tagIDToImageTag), } for _, rule := range fan.MatchedRules { @@ -271,7 +306,7 @@ func fileActivityNodeToProto(fan *FileNode) *adproto.FileActivityNode { } for _, child := range fan.Children { - pfan.Children = append(pfan.Children, fileActivityNodeToProto(child)) + pfan.Children = append(pfan.Children, fileActivityNodeToProto(child, tagIDToImageTag)) } return pfan @@ -291,7 +326,7 @@ func openNodeToProto(openNode *OpenNode) *adproto.OpenNode { return pon } -func dnsNodeToProto(dn *DNSNode) *adproto.DNSNode { +func dnsNodeToProto(dn *DNSNode, tagIDToImageTag func(id uint64) string) *adproto.DNSNode { if dn == nil { return nil } @@ -309,7 +344,7 @@ func dnsNodeToProto(dn *DNSNode) *adproto.DNSNode { pdn.Requests = append(pdn.Requests, dnsEventToProto(&req)) } - pdn.NodeBase = nodeBaseToProto(&dn.NodeBase) + pdn.NodeBase = nodeBaseToProto(&dn.NodeBase, tagIDToImageTag) return pdn } @@ -328,14 +363,14 @@ func dnsEventToProto(ev *model.DNSEvent) *adproto.DNSInfo { } } -func imdsNodeToProto(in *IMDSNode) *adproto.IMDSNode { +func imdsNodeToProto(in *IMDSNode, tagIDToImageTag func(id uint64) string) *adproto.IMDSNode { if in == nil { return nil } pin := &adproto.IMDSNode{ MatchedRules: make([]*adproto.MatchedRule, 0, len(in.MatchedRules)), - NodeBase: nodeBaseToProto(&in.NodeBase), + NodeBase: nodeBaseToProto(&in.NodeBase, tagIDToImageTag), Event: imdsEventToProto(in.Event), } @@ -370,7 +405,7 @@ func awsIMDSEventToProto(event model.IMDSEvent) *adproto.AWSIMDSEvent { } } -func socketNodeToProto(sn *SocketNode) *adproto.SocketNode { +func socketNodeToProto(sn *SocketNode, tagIDToImageTag func(id uint64) string) *adproto.SocketNode { if sn == nil { return nil } @@ -386,7 +421,7 @@ func socketNodeToProto(sn *SocketNode) *adproto.SocketNode { Port: uint32(bn.Port), Ip: bn.IP, Protocol: uint32(bn.Protocol), - NodeBase: nodeBaseToProto(&bn.NodeBase), + NodeBase: nodeBaseToProto(&bn.NodeBase, tagIDToImageTag), } for _, rule := range bn.MatchedRules { @@ -437,32 +472,38 @@ func escape(in string) string { return transformer.String(in) } -func nodeBaseToProto(nb *NodeBase) *adproto.NodeBase { +func nodeBaseToProto(nb *NodeBase, tagIDToImageTag func(id uint64) string) *adproto.NodeBase { if nb == nil { return nil } pnb := &adproto.NodeBase{ - Seen: make(map[string]*adproto.ImageTagTimes, len(nb.Seen)), + Seen: make(map[string]*adproto.ImageTagTimes, nb.SeenLen()), } - for imageTag, times := range nb.Seen { - pnb.Seen[imageTag] = &adproto.ImageTagTimes{ + nb.EachSeen(func(id uint64, times ImageTagTimes) { + tag := tagIDToImageTag(id) + if tag == "" { + // ID is stale (slot was freed before this node was evicted); skip to avoid + // writing an empty key into the proto map. + return + } + pnb.Seen[tag] = &adproto.ImageTagTimes{ FirstSeen: TimestampToProto(×.FirstSeen), LastSeen: TimestampToProto(×.LastSeen), } - } + }) return pnb } -func capabilityNodeToProto(cap *CapabilityNode) *adproto.CapabilityNode { +func capabilityNodeToProto(cap *CapabilityNode, tagIDToImageTag func(id uint64) string) *adproto.CapabilityNode { if cap == nil { return nil } return &adproto.CapabilityNode{ - NodeBase: nodeBaseToProto(&cap.NodeBase), + NodeBase: nodeBaseToProto(&cap.NodeBase, tagIDToImageTag), Capability: cap.Capability, IsCapable: cap.Capable, } diff --git a/pkg/security/security_profile/activity_tree/activity_tree_test.go b/pkg/security/security_profile/activity_tree/activity_tree_test.go index b0699d5ec1ea..14ba8df6c44d 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree_test.go +++ b/pkg/security/security_profile/activity_tree/activity_tree_test.go @@ -63,7 +63,7 @@ func TestInsertFileEvent(t *testing.T) { }, }, } - pan.InsertFileEvent(&event.Open.File, event, "tag", Unknown, stats, false, nil, nil) + pan.InsertFileEvent(&event.Open.File, event, uint64(666), Unknown, stats, false, nil, nil) } var builder strings.Builder @@ -335,6 +335,8 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { Stats: NewActivityTreeNodeStats(), } + testTagID := tree.GetOrInsertImageTag("test-tag") + // Create a process node with an old "last seen" timestamp oldTime := time.Now().Add(-2 * time.Hour) processNode := &ProcessNode{ @@ -345,7 +347,7 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { }, }, } - processNode.AppendImageTag("test-tag", oldTime) + processNode.AppendImageTagID(testTagID, oldTime) tree.ProcessNodes = []*ProcessNode{processNode} // Set eviction time to 1 hour ago (node should be evicted) @@ -369,6 +371,8 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { Stats: NewActivityTreeNodeStats(), } + testTagID := tree.GetOrInsertImageTag("test-tag") + // Create a process node with an old "last seen" timestamp oldTime := time.Now().Add(-2 * time.Hour) processNode := &ProcessNode{ @@ -379,7 +383,8 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { }, }, } - processNode.AppendImageTag("test-tag", oldTime) + + processNode.AppendImageTagID(testTagID, oldTime) tree.ProcessNodes = []*ProcessNode{processNode} // Set eviction time to 1 hour ago (node would normally be evicted) @@ -398,8 +403,8 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { assert.Len(t, tree.ProcessNodes, 1, "Expected process node to remain in tree") // Verify that the LastSeen timestamp was updated to protect the node - imageTagTimes := processNode.Seen["test-tag"] - assert.NotNil(t, imageTagTimes, "Expected image tag to still exist") + imageTagTimes, exists := processNode.GetSeenTimes(testTagID) + assert.True(t, exists, "Expected image tag to still exist") assert.True(t, imageTagTimes.LastSeen.After(evictionTime), "Expected LastSeen to be updated to current time") }) @@ -413,6 +418,8 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { // Create process nodes with old timestamps oldTime := time.Now().Add(-2 * time.Hour) + testTagID := tree.GetOrInsertImageTag("test-tag") + protectedNode := &ProcessNode{ NodeBase: NewNodeBase(), Process: model.Process{ @@ -421,7 +428,8 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { }, }, } - protectedNode.AppendImageTag("test-tag", oldTime) + + protectedNode.AppendImageTagID(testTagID, oldTime) expiredNode := &ProcessNode{ NodeBase: NewNodeBase(), @@ -431,7 +439,8 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { }, }, } - expiredNode.AppendImageTag("test-tag", oldTime) + + expiredNode.AppendImageTagID(testTagID, oldTime) tree.ProcessNodes = []*ProcessNode{protectedNode, expiredNode} @@ -452,8 +461,8 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { assert.Equal(t, "/usr/bin/protected", tree.ProcessNodes[0].Process.FileEvent.PathnameStr, "Expected protected node to remain") // Verify that the protected node's timestamp was updated - imageTagTimes := tree.ProcessNodes[0].Seen["test-tag"] - assert.NotNil(t, imageTagTimes, "Expected image tag to still exist") + imageTagTimes, exists := tree.ProcessNodes[0].GetSeenTimes(testTagID) + assert.True(t, exists, "Expected image tag to still exist") assert.True(t, imageTagTimes.LastSeen.After(evictionTime), "Expected LastSeen to be updated to current time") }) @@ -469,6 +478,11 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { oldTime := time.Now().Add(-2 * time.Hour) recentTime := time.Now().Add(-30 * time.Minute) + veryOldTagID := tree.GetOrInsertImageTag("very-old-tag") + oldTagID := tree.GetOrInsertImageTag("old-tag") + recentTagID := tree.GetOrInsertImageTag("recent-tag") + testTagID := tree.GetOrInsertImageTag("test-tag") + processNode := &ProcessNode{ NodeBase: NewNodeBase(), Process: model.Process{ @@ -477,10 +491,11 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { }, }, } - processNode.AppendImageTag("very-old-tag", veryOldTime) - processNode.AppendImageTag("old-tag", oldTime) - processNode.AppendImageTag("recent-tag", recentTime) - processNode.AppendImageTag("test-tag", oldTime) // Add the profile tag that can be refreshed + + processNode.AppendImageTagID(veryOldTagID, veryOldTime) + processNode.AppendImageTagID(oldTagID, oldTime) + processNode.AppendImageTagID(recentTagID, recentTime) + processNode.AppendImageTagID(testTagID, oldTime) // Add the profile tag that can be refreshed tree.ProcessNodes = []*ProcessNode{processNode} // Set eviction time to 1 hour ago (very-old-tag and old-tag should be evicted) @@ -500,16 +515,16 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { // Verify that only the profile's image tag was refreshed node := tree.ProcessNodes[0] - veryOldTagTimes := node.Seen["very-old-tag"] - oldTagTimes := node.Seen["old-tag"] - recentTagTimes := node.Seen["recent-tag"] - testTagTimes := node.Seen["test-tag"] + veryOldTagTimes, _ := node.GetSeenTimes(veryOldTagID) + oldTagTimes, _ := node.GetSeenTimes(oldTagID) + recentTagTimes, _ := node.GetSeenTimes(recentTagID) + testTagTimes, _ := node.GetSeenTimes(testTagID) // The very-old-tag and old-tag should have been evicted since they weren't refreshed - assert.Nil(t, veryOldTagTimes, "Expected very-old-tag to be evicted") - assert.Nil(t, oldTagTimes, "Expected old-tag to be evicted") - assert.NotNil(t, recentTagTimes, "Expected recent-tag to still exist") - assert.NotNil(t, testTagTimes, "Expected test-tag to still exist") + assert.Zero(t, veryOldTagTimes, "Expected very-old-tag to be evicted") + assert.Zero(t, oldTagTimes, "Expected old-tag to be evicted") + assert.NotZero(t, recentTagTimes, "Expected recent-tag to still exist") + assert.NotZero(t, testTagTimes, "Expected test-tag to still exist") // The test-tag should have been refreshed to current time (it's the profile tag) assert.True(t, testTagTimes.LastSeen.After(evictionTime), "Expected test-tag LastSeen to be updated") @@ -535,7 +550,8 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { }, }, } - node1.AppendImageTag("test-tag", oldTime) + testTagID := uint64(666) + node1.AppendImageTagID(testTagID, oldTime) node2 := &ProcessNode{ NodeBase: NewNodeBase(), @@ -545,7 +561,7 @@ func TestEvictUnusedNodes_ProcessCacheProtection(t *testing.T) { }, }, } - node2.AppendImageTag("test-tag", oldTime) + node2.AppendImageTagID(testTagID, oldTime) tree.ProcessNodes = []*ProcessNode{node1, node2} diff --git a/pkg/security/security_profile/activity_tree/base_node.go b/pkg/security/security_profile/activity_tree/base_node.go index 257fed540769..ccb6f72b6c88 100644 --- a/pkg/security/security_profile/activity_tree/base_node.go +++ b/pkg/security/security_profile/activity_tree/base_node.go @@ -18,58 +18,118 @@ type ImageTagTimes struct { LastSeen time.Time } -// NodeBase provides the base functionality for all nodes in the activity tree +// seenEntry pairs an image tag ID with its observation timestamps. +type seenEntry struct { + id uint64 + times ImageTagTimes +} + +// NodeBase provides the base functionality for all nodes in the activity tree. type NodeBase struct { - Seen map[string]*ImageTagTimes // imageTag → timestamps + seen []seenEntry } // NewNodeBase creates a new NodeBase instance func NewNodeBase() NodeBase { - return NodeBase{Seen: make(map[string]*ImageTagTimes)} + return NodeBase{} } -// AppendImageTag adds a new entry in the map or updates the LastSeen for the given imageTag at time 'now'. -func (b *NodeBase) AppendImageTag(imageTag string, timestamp time.Time) { - if imageTag == "" { +// AppendImageTagID adds a new entry in the slice or updates the LastSeen for the given imageTagID. +// ID 0 is the null sentinel and is a no-op. +func (b *NodeBase) AppendImageTagID(imageTagID uint64, timestamp time.Time) { + if imageTagID == 0 { return } - if vt, ok := b.Seen[imageTag]; ok { - vt.LastSeen = timestamp - return + for i, entry := range b.seen { + if entry.id == imageTagID { + b.seen[i].times.LastSeen = timestamp + return + } } - b.Seen[imageTag] = &ImageTagTimes{FirstSeen: timestamp, LastSeen: timestamp} + // Three-index slice sets cap == len before append so Go allocates exactly one new slot, + // avoiding the default doubling strategy for a structure that stays small (typically ≤5 entries). + b.seen = append(b.seen[:len(b.seen):len(b.seen)], seenEntry{id: imageTagID, times: ImageTagTimes{FirstSeen: timestamp, LastSeen: timestamp}}) } -// RecordWithTimestamps sets both FirstSeen and LastSeen for the given imageTag with the provided timestamps. -func (b *NodeBase) RecordWithTimestamps(imageTag string, firstSeen, lastSeen time.Time) { - b.Seen[imageTag] = &ImageTagTimes{FirstSeen: firstSeen, LastSeen: lastSeen} +// RecordWithTimestamps sets both FirstSeen and LastSeen for the given imageTagID with the provided timestamps. +// ID 0 is the null sentinel and is a no-op. +func (b *NodeBase) RecordWithTimestamps(imageTagID uint64, firstSeen, lastSeen time.Time) { + if imageTagID == 0 { + return + } + for i, entry := range b.seen { + if entry.id == imageTagID { + b.seen[i].times = ImageTagTimes{FirstSeen: firstSeen, LastSeen: lastSeen} + return + } + } + b.seen = append(b.seen[:len(b.seen):len(b.seen)], seenEntry{id: imageTagID, times: ImageTagTimes{FirstSeen: firstSeen, LastSeen: lastSeen}}) } -// EvictImageTag removes the stored timestamps for an imageTag returns false if the imageTag was not present or if the imageTag is empty -// returns true if the imageTag was present and the map is now empty -func (b *NodeBase) EvictImageTag(imageTag string) bool { - if !b.HasImageTag(imageTag) || imageTag == "" { - return false +// EvictImageTag removes the entry for imageTagID and returns true if the slice is now empty. +// Returns false if imageTagID was not present. +func (b *NodeBase) EvictImageTag(imageTagID uint64) bool { + for i, entry := range b.seen { + if entry.id == imageTagID { + // swap-and-truncate — order doesn't matter for this structure + b.seen[i] = b.seen[len(b.seen)-1] + b.seen = b.seen[:len(b.seen)-1] + return len(b.seen) == 0 + } } - delete(b.Seen, imageTag) - return len(b.Seen) == 0 + return false } -// EvictBeforeTimestamp removes all imageTags whose LastSeen is before the given timestamp. -// Returns the number of imageTags that were removed. +// EvictBeforeTimestamp removes all entries whose LastSeen is before the given timestamp. +// Returns the number of entries removed. func (b *NodeBase) EvictBeforeTimestamp(before time.Time) int { removed := 0 - for imageTag, times := range b.Seen { - if times.LastSeen.Before(before) { - delete(b.Seen, imageTag) + i := 0 + for i < len(b.seen) { + if b.seen[i].times.LastSeen.Before(before) { + b.seen[i] = b.seen[len(b.seen)-1] + b.seen = b.seen[:len(b.seen)-1] removed++ + } else { + i++ } } return removed } -// HasImageTag returns true if the imageTag exists in the Seen map. -func (b *NodeBase) HasImageTag(imageTag string) bool { - _, exists := b.Seen[imageTag] - return exists +// HasImageTag returns true if imageTagID has an entry in the slice. +func (b *NodeBase) HasImageTag(imageTagID uint64) bool { + for _, entry := range b.seen { + if entry.id == imageTagID { + return true + } + } + return false +} + +// SeenIsEmpty returns true if no image tags are recorded. +func (b *NodeBase) SeenIsEmpty() bool { + return len(b.seen) == 0 +} + +// SeenLen returns the number of recorded image tags. +func (b *NodeBase) SeenLen() int { + return len(b.seen) +} + +// GetSeenTimes returns the timestamps for the given imageTagID, or the zero value and false if not found. +func (b *NodeBase) GetSeenTimes(imageTagID uint64) (ImageTagTimes, bool) { + for _, entry := range b.seen { + if entry.id == imageTagID { + return entry.times, true + } + } + return ImageTagTimes{}, false +} + +// EachSeen calls fn for every recorded image tag ID and its timestamps. +func (b *NodeBase) EachSeen(fn func(id uint64, times ImageTagTimes)) { + for _, entry := range b.seen { + fn(entry.id, entry.times) + } } diff --git a/pkg/security/security_profile/activity_tree/capability_node.go b/pkg/security/security_profile/activity_tree/capability_node.go index 928092c8938f..f9f2d62c0a8f 100644 --- a/pkg/security/security_profile/activity_tree/capability_node.go +++ b/pkg/security/security_profile/activity_tree/capability_node.go @@ -20,9 +20,9 @@ type CapabilityNode struct { } // NewCapabilityNode creates a new CapabilityNode -func NewCapabilityNode(capability uint64, capable bool, timestamp time.Time, imageTag string, generationType NodeGenerationType) *CapabilityNode { +func NewCapabilityNode(capability uint64, capable bool, timestamp time.Time, imageTagID uint64, generationType NodeGenerationType) *CapabilityNode { nodeBase := NewNodeBase() - nodeBase.AppendImageTag(imageTag, timestamp) + nodeBase.AppendImageTagID(imageTagID, timestamp) return &CapabilityNode{ NodeBase: nodeBase, diff --git a/pkg/security/security_profile/activity_tree/dns_node.go b/pkg/security/security_profile/activity_tree/dns_node.go index 7332699b76d8..34a33aac23ca 100644 --- a/pkg/security/security_profile/activity_tree/dns_node.go +++ b/pkg/security/security_profile/activity_tree/dns_node.go @@ -24,14 +24,14 @@ type DNSNode struct { } // NewDNSNode returns a new DNSNode instance -func NewDNSNode(event *model.DNSEvent, evt *model.Event, rules []*model.MatchedRule, generationType NodeGenerationType, imageTag string) *DNSNode { +func NewDNSNode(event *model.DNSEvent, evt *model.Event, rules []*model.MatchedRule, generationType NodeGenerationType, imageTagID uint64) *DNSNode { node := &DNSNode{ MatchedRules: rules, GenerationType: generationType, Requests: []model.DNSEvent{*event}, } node.NodeBase = NewNodeBase() - node.AppendImageTag(imageTag, evt.ResolveEventTime()) + node.AppendImageTagID(imageTagID, evt.ResolveEventTime()) return node } @@ -50,8 +50,8 @@ func dnsFilterSubdomains(name string, maxDepth int) string { return result } -func (dn *DNSNode) evictImageTag(imageTag string, DNSNames *utils.StringKeys) bool { - IsNodeEmpty := dn.EvictImageTag(imageTag) +func (dn *DNSNode) evictImageTag(imageTagID uint64, DNSNames *utils.StringKeys) bool { + IsNodeEmpty := dn.EvictImageTag(imageTagID) if IsNodeEmpty { return true } diff --git a/pkg/security/security_profile/activity_tree/file_node.go b/pkg/security/security_profile/activity_tree/file_node.go index 5c79b37822fc..db81ded35347 100644 --- a/pkg/security/security_profile/activity_tree/file_node.go +++ b/pkg/security/security_profile/activity_tree/file_node.go @@ -20,13 +20,32 @@ import ( "github.com/DataDog/datadog-agent/pkg/security/secl/model" ) +// storedFileEvent holds the subset of model.FileEvent fields persisted in the +// activity tree. Transient resolution state (mount paths, resolution flags, +// path resolution errors) is intentionally omitted. +type storedFileEvent struct { + model.FileFields + PathnameStr string + BasenameStr string + Filesystem string + PkgName string + PkgVersion string + PkgEpoch int + PkgRelease string + PkgSrcVersion string + PkgSrcEpoch int + PkgSrcRelease string + Hashes []string + HashState model.HashState +} + // FileNode holds a tree representation of a list of files type FileNode struct { NodeBase MatchedRules []*model.MatchedRule Name string IsPattern bool - File *model.FileEvent + File *storedFileEvent GenerationType NodeGenerationType Open *OpenNode @@ -41,27 +60,46 @@ type OpenNode struct { } // NewFileNode returns a new FileActivityNode instance -func NewFileNode(fileEvent *model.FileEvent, event *model.Event, name string, imageTag string, generationType NodeGenerationType, reducedFilePath string, resolvers *resolvers.EBPFResolvers) *FileNode { +func NewFileNode(fileEvent *model.FileEvent, event *model.Event, name string, imageTagID uint64, generationType NodeGenerationType, reducedFilePath string, resolvers *resolvers.EBPFResolvers) *FileNode { // call resolver. Safeguard: the process context might be empty if from a snapshot. if resolvers != nil && fileEvent != nil && event.ProcessContext != nil { resolvers.HashResolver.ComputeHashesFromEvent(event, fileEvent, 0) } + if resolvers != nil { + if resolvers.BasenameInterner != nil { + name = resolvers.BasenameInterner.Deduplicate(name) + } + if resolvers.PathInterner != nil && reducedFilePath != "" { + reducedFilePath = resolvers.PathInterner.Deduplicate(reducedFilePath) + } + } + fan := &FileNode{ Name: name, GenerationType: generationType, IsPattern: strings.Contains(name, "*"), - Children: make(map[string]*FileNode), } fan.NodeBase = NewNodeBase() if event != nil { - fan.AppendImageTag(imageTag, event.ResolveEventTime()) + fan.AppendImageTagID(imageTagID, event.ResolveEventTime()) } if fileEvent != nil { - fileEventTmp := *fileEvent - fan.File = &fileEventTmp - fan.File.PathnameStr = reducedFilePath - fan.File.BasenameStr = name + fan.File = &storedFileEvent{ + FileFields: fileEvent.FileFields, + PathnameStr: reducedFilePath, + BasenameStr: name, + Filesystem: fileEvent.Filesystem, + PkgName: fileEvent.PkgName, + PkgVersion: fileEvent.PkgVersion, + PkgEpoch: fileEvent.PkgEpoch, + PkgRelease: fileEvent.PkgRelease, + PkgSrcVersion: fileEvent.PkgSrcVersion, + PkgSrcEpoch: fileEvent.PkgSrcEpoch, + PkgSrcRelease: fileEvent.PkgSrcRelease, + Hashes: fileEvent.Hashes, + HashState: fileEvent.HashState, + } } fan.enrichFromEvent(event) return fan @@ -146,7 +184,7 @@ func (fn *FileNode) debug(w io.Writer, prefix string) { // InsertFileEvent inserts an event in a FileNode. This function returns true if a new entry was added, false if // the event was dropped. -func (fn *FileNode) InsertFileEvent(fileEvent *model.FileEvent, event *model.Event, remainingPath string, imageTag string, generationType NodeGenerationType, stats *Stats, dryRun bool, reducedPath string, resolvers *resolvers.EBPFResolvers) bool { +func (fn *FileNode) InsertFileEvent(fileEvent *model.FileEvent, event *model.Event, remainingPath string, imageTagID uint64, generationType NodeGenerationType, stats *Stats, dryRun bool, reducedPath string, resolvers *resolvers.EBPFResolvers) bool { currentFn := fn currentPath := remainingPath newEntry := false @@ -164,7 +202,7 @@ func (fn *FileNode) InsertFileEvent(fileEvent *model.FileEvent, event *model.Eve if ok { currentFn = child currentPath = currentPath[nextParentIndex:] - currentFn.AppendImageTag(imageTag, event.ResolveEventTime()) + currentFn.AppendImageTagID(imageTagID, event.ResolveEventTime()) continue } @@ -173,12 +211,15 @@ func (fn *FileNode) InsertFileEvent(fileEvent *model.FileEvent, event *model.Eve if dryRun { break } + if currentFn.Children == nil { + currentFn.Children = make(map[string]*FileNode) + } if len(currentPath) <= nextParentIndex+1 { - currentFn.Children[parent] = NewFileNode(fileEvent, event, parent, imageTag, generationType, reducedPath, resolvers) + currentFn.Children[parent] = NewFileNode(fileEvent, event, parent, imageTagID, generationType, reducedPath, resolvers) stats.FileNodes++ break } - newChild := NewFileNode(nil, nil, parent, imageTag, generationType, "", resolvers) + newChild := NewFileNode(nil, nil, parent, imageTagID, generationType, "", resolvers) currentFn.Children[parent] = newChild currentFn = newChild currentPath = currentPath[nextParentIndex:] @@ -186,23 +227,23 @@ func (fn *FileNode) InsertFileEvent(fileEvent *model.FileEvent, event *model.Eve return newEntry } -func (fn *FileNode) tagAllNodes(imageTag string, timestamp time.Time) { - fn.AppendImageTag(imageTag, timestamp) +func (fn *FileNode) tagAllNodes(imageTagID uint64, timestamp time.Time) { + fn.AppendImageTagID(imageTagID, timestamp) for _, child := range fn.Children { - child.tagAllNodes(imageTag, timestamp) + child.tagAllNodes(imageTagID, timestamp) } } -func (fn *FileNode) evictImageTag(imageTag string) bool { - if !fn.HasImageTag(imageTag) { +func (fn *FileNode) evictImageTag(imageTagID uint64) bool { + if !fn.HasImageTag(imageTagID) { return false } - evicted := fn.EvictImageTag(imageTag) + evicted := fn.EvictImageTag(imageTagID) if evicted { return true } for filename, child := range fn.Children { - if shouldRemoveNode := child.evictImageTag(imageTag); shouldRemoveNode { + if shouldRemoveNode := child.evictImageTag(imageTagID); shouldRemoveNode { delete(fn.Children, filename) } } diff --git a/pkg/security/security_profile/activity_tree/flow_node.go b/pkg/security/security_profile/activity_tree/flow_node.go index 0877a834c17e..35bf3af0f6bc 100644 --- a/pkg/security/security_profile/activity_tree/flow_node.go +++ b/pkg/security/security_profile/activity_tree/flow_node.go @@ -20,18 +20,18 @@ type FlowNode struct { } // NewFlowNode returns a new FlowNode instance -func NewFlowNode(flow model.Flow, event *model.Event, generationType NodeGenerationType, imageTag string) *FlowNode { +func NewFlowNode(flow model.Flow, event *model.Event, generationType NodeGenerationType, imageTagID uint64) *FlowNode { node := &FlowNode{ GenerationType: generationType, Flow: flow, } node.NodeBase = NewNodeBase() - node.AppendImageTag(imageTag, event.ResolveEventTime()) + node.AppendImageTagID(imageTagID, event.ResolveEventTime()) return node } -func (node *FlowNode) addFlow(flow model.Flow, event *model.Event, imageTag string) { - node.AppendImageTag(imageTag, event.ResolveEventTime()) +func (node *FlowNode) addFlow(flow model.Flow, event *model.Event, imageTagID uint64) { + node.AppendImageTagID(imageTagID, event.ResolveEventTime()) // add metrics node.Flow.Egress.Add(flow.Egress) diff --git a/pkg/security/security_profile/activity_tree/imds_node.go b/pkg/security/security_profile/activity_tree/imds_node.go index f2256e4eb572..b3cb540aea41 100644 --- a/pkg/security/security_profile/activity_tree/imds_node.go +++ b/pkg/security/security_profile/activity_tree/imds_node.go @@ -21,14 +21,14 @@ type IMDSNode struct { } // NewIMDSNode creates a new IMDSNode instance -func NewIMDSNode(event *model.IMDSEvent, evt *model.Event, rules []*model.MatchedRule, generationType NodeGenerationType, imageTag string) *IMDSNode { +func NewIMDSNode(event *model.IMDSEvent, evt *model.Event, rules []*model.MatchedRule, generationType NodeGenerationType, imageTagID uint64) *IMDSNode { node := &IMDSNode{ MatchedRules: rules, GenerationType: generationType, Event: *event, } node.NodeBase = NewNodeBase() - node.AppendImageTag(imageTag, evt.ResolveEventTime()) + node.AppendImageTagID(imageTagID, evt.ResolveEventTime()) return node } diff --git a/pkg/security/security_profile/activity_tree/network_device_node.go b/pkg/security/security_profile/activity_tree/network_device_node.go index 7c052c851409..77897222efc1 100644 --- a/pkg/security/security_profile/activity_tree/network_device_node.go +++ b/pkg/security/security_profile/activity_tree/network_device_node.go @@ -33,15 +33,15 @@ func NewNetworkDeviceNode(ctx *model.NetworkDeviceContext, generationType NodeGe return node } -func (netdevice *NetworkDeviceNode) appendImageTag(imageTag string, timestamp time.Time) { +func (netdevice *NetworkDeviceNode) appendImageTag(imageTagID uint64, timestamp time.Time) { for _, flow := range netdevice.FlowNodes { - flow.AppendImageTag(imageTag, timestamp) + flow.AppendImageTagID(imageTagID, timestamp) } } -func (netdevice *NetworkDeviceNode) evictImageTag(imageTag string) bool { +func (netdevice *NetworkDeviceNode) evictImageTag(imageTagID uint64) bool { for key, flow := range netdevice.FlowNodes { - if flow.EvictImageTag(imageTag) { + if flow.EvictImageTag(imageTagID) { delete(netdevice.FlowNodes, key) } } @@ -49,7 +49,7 @@ func (netdevice *NetworkDeviceNode) evictImageTag(imageTag string) bool { return len(netdevice.FlowNodes) == 0 } -func (netdevice *NetworkDeviceNode) insertNetworkFlowMonitorEvent(event *model.NetworkFlowMonitorEvent, evt *model.Event, dryRun bool, rules []*model.MatchedRule, generationType NodeGenerationType, imageTag string, stats *Stats) bool { +func (netdevice *NetworkDeviceNode) insertNetworkFlowMonitorEvent(event *model.NetworkFlowMonitorEvent, evt *model.Event, dryRun bool, rules []*model.MatchedRule, generationType NodeGenerationType, imageTagID uint64, stats *Stats) bool { if len(rules) > 0 { netdevice.MatchedRules = model.AppendMatchedRule(netdevice.MatchedRules, rules) } @@ -59,7 +59,7 @@ func (netdevice *NetworkDeviceNode) insertNetworkFlowMonitorEvent(event *model.N existingNode, ok := netdevice.FlowNodes[flow.GetFiveTuple()] if ok { if !dryRun { - existingNode.addFlow(flow, evt, imageTag) + existingNode.addFlow(flow, evt, imageTagID) } } else { newFlow = true @@ -68,7 +68,7 @@ func (netdevice *NetworkDeviceNode) insertNetworkFlowMonitorEvent(event *model.N return newFlow } // create new entry - netdevice.FlowNodes[flow.GetFiveTuple()] = NewFlowNode(flow, evt, generationType, imageTag) + netdevice.FlowNodes[flow.GetFiveTuple()] = NewFlowNode(flow, evt, generationType, imageTagID) stats.FlowNodes++ } } diff --git a/pkg/security/security_profile/activity_tree/process_node.go b/pkg/security/security_profile/activity_tree/process_node.go index 09f7f5e478a9..404621d52302 100644 --- a/pkg/security/security_profile/activity_tree/process_node.go +++ b/pkg/security/security_profile/activity_tree/process_node.go @@ -32,7 +32,7 @@ type ProcessNodeParent interface { GetChildren() *[]*ProcessNode GetSiblings() *[]*ProcessNode AppendChild(node *ProcessNode) - AppendImageTag(imageTag string, timestamp time.Time) + AppendImageTagID(imageTagID uint64, timestamp time.Time) } // ProcessNode holds the activity of a process @@ -220,13 +220,13 @@ func (pn *ProcessNode) Matches(entry *model.Process, matchArgs bool, normalize b } // InsertSyscalls inserts the syscall of the process in the dump -func (pn *ProcessNode) InsertSyscalls(e *model.Event, imageTag string, syscallMask map[int]int, stats *Stats, dryRun bool) bool { +func (pn *ProcessNode) InsertSyscalls(e *model.Event, imageTagID uint64, syscallMask map[int]int, stats *Stats, dryRun bool) bool { var hasNewSyscalls bool newSyscallLoop: for _, newSyscall := range e.Syscalls.Syscalls { for _, existingSyscall := range pn.Syscalls { if existingSyscall.Syscall == int(newSyscall) { - existingSyscall.AppendImageTag(imageTag, e.ResolveEventTime()) + existingSyscall.AppendImageTagID(imageTagID, e.ResolveEventTime()) continue newSyscallLoop } } @@ -236,7 +236,7 @@ newSyscallLoop: // exit early break } - pn.Syscalls = append(pn.Syscalls, NewSyscallNode(int(newSyscall), e.ResolveEventTime(), imageTag, Runtime)) + pn.Syscalls = append(pn.Syscalls, NewSyscallNode(int(newSyscall), e.ResolveEventTime(), imageTagID, Runtime)) syscallMask[int(newSyscall)] = int(newSyscall) stats.SyscallNodes++ } @@ -246,7 +246,7 @@ newSyscallLoop: // InsertFileEvent inserts the provided file event in the current node. This function returns true if a new entry was // added, false if the event was dropped. -func (pn *ProcessNode) InsertFileEvent(fileEvent *model.FileEvent, event *model.Event, imageTag string, generationType NodeGenerationType, stats *Stats, dryRun bool, reducer *PathsReducer, resolvers *resolvers.EBPFResolvers) bool { +func (pn *ProcessNode) InsertFileEvent(fileEvent *model.FileEvent, event *model.Event, imageTagID uint64, generationType NodeGenerationType, stats *Stats, dryRun bool, reducer *PathsReducer, resolvers *resolvers.EBPFResolvers) bool { var filePath string if generationType != Snapshot { filePath = event.FieldHandlers.ResolveFilePath(event, fileEvent) @@ -265,22 +265,22 @@ func (pn *ProcessNode) InsertFileEvent(fileEvent *model.FileEvent, event *model. child, ok := pn.Files[parent] if ok { - return child.InsertFileEvent(fileEvent, event, filePath[nextParentIndex:], imageTag, generationType, stats, dryRun, filePath, resolvers) + return child.InsertFileEvent(fileEvent, event, filePath[nextParentIndex:], imageTagID, generationType, stats, dryRun, filePath, resolvers) } if !dryRun { // create new child if len(filePath) <= nextParentIndex+1 { // this is the last child, add the fileEvent context at the leaf of the files tree. - node := NewFileNode(fileEvent, event, parent, imageTag, generationType, filePath, resolvers) + node := NewFileNode(fileEvent, event, parent, imageTagID, generationType, filePath, resolvers) node.MatchedRules = model.AppendMatchedRule(node.MatchedRules, event.Rules) stats.FileNodes++ pn.Files[parent] = node } else { // This is an intermediary node in the branch that leads to the leaf we want to add. Create a node without the // fileEvent context. - newChild := NewFileNode(nil, nil, parent, imageTag, generationType, filePath, resolvers) - newChild.InsertFileEvent(fileEvent, event, filePath[nextParentIndex:], imageTag, generationType, stats, dryRun, filePath, resolvers) + newChild := NewFileNode(nil, nil, parent, imageTagID, generationType, filePath, resolvers) + newChild.InsertFileEvent(fileEvent, event, filePath[nextParentIndex:], imageTagID, generationType, stats, dryRun, filePath, resolvers) stats.FileNodes++ pn.Files[parent] = newChild } @@ -308,7 +308,7 @@ func (pn *ProcessNode) findDNSNode(DNSName string, DNSMatchMaxDepth int, DNSType } // InsertDNSEvent inserts a DNS event in a process node -func (pn *ProcessNode) InsertDNSEvent(evt *model.Event, imageTag string, generationType NodeGenerationType, stats *Stats, DNSNames *utils.StringKeys, dryRun bool, dnsMatchMaxDepth int) bool { +func (pn *ProcessNode) InsertDNSEvent(evt *model.Event, imageTagID uint64, generationType NodeGenerationType, stats *Stats, DNSNames *utils.StringKeys, dryRun bool, dnsMatchMaxDepth int) bool { if dryRun { // Use DNSMatchMaxDepth only when searching for a node, not when trying to insert return !pn.findDNSNode(evt.DNS.Question.Name, dnsMatchMaxDepth, evt.DNS.Question.Type) @@ -320,7 +320,7 @@ func (pn *ProcessNode) InsertDNSEvent(evt *model.Event, imageTag string, generat // update matched rules dnsNode.MatchedRules = model.AppendMatchedRule(dnsNode.MatchedRules, evt.Rules) - dnsNode.AppendImageTag(imageTag, evt.ResolveEventTime()) + dnsNode.AppendImageTagID(imageTagID, evt.ResolveEventTime()) // look for the DNS request type for _, req := range dnsNode.Requests { @@ -334,45 +334,45 @@ func (pn *ProcessNode) InsertDNSEvent(evt *model.Event, imageTag string, generat return true } - pn.DNSNames[evt.DNS.Question.Name] = NewDNSNode(&evt.DNS, evt, evt.Rules, generationType, imageTag) + pn.DNSNames[evt.DNS.Question.Name] = NewDNSNode(&evt.DNS, evt, evt.Rules, generationType, imageTagID) stats.DNSNodes++ return true } // InsertIMDSEvent inserts an IMDS event in a process node -func (pn *ProcessNode) InsertIMDSEvent(evt *model.Event, imageTag string, generationType NodeGenerationType, stats *Stats, dryRun bool) bool { +func (pn *ProcessNode) InsertIMDSEvent(evt *model.Event, imageTagID uint64, generationType NodeGenerationType, stats *Stats, dryRun bool) bool { imdsNode, ok := pn.IMDSEvents[evt.IMDS] if ok { imdsNode.MatchedRules = model.AppendMatchedRule(imdsNode.MatchedRules, evt.Rules) - imdsNode.AppendImageTag(imageTag, evt.ResolveEventTime()) + imdsNode.AppendImageTagID(imageTagID, evt.ResolveEventTime()) return false } if !dryRun { // create new node - pn.IMDSEvents[evt.IMDS] = NewIMDSNode(&evt.IMDS, evt, evt.Rules, generationType, imageTag) + pn.IMDSEvents[evt.IMDS] = NewIMDSNode(&evt.IMDS, evt, evt.Rules, generationType, imageTagID) stats.IMDSNodes++ } return true } // InsertNetworkFlowMonitorEvent inserts a Network Flow Monitor event in a process node -func (pn *ProcessNode) InsertNetworkFlowMonitorEvent(evt *model.Event, imageTag string, generationType NodeGenerationType, stats *Stats, dryRun bool) bool { +func (pn *ProcessNode) InsertNetworkFlowMonitorEvent(evt *model.Event, imageTagID uint64, generationType NodeGenerationType, stats *Stats, dryRun bool) bool { deviceNode, ok := pn.NetworkDevices[evt.NetworkFlowMonitor.Device] if ok { - return deviceNode.insertNetworkFlowMonitorEvent(&evt.NetworkFlowMonitor, evt, dryRun, evt.Rules, generationType, imageTag, stats) + return deviceNode.insertNetworkFlowMonitorEvent(&evt.NetworkFlowMonitor, evt, dryRun, evt.Rules, generationType, imageTagID, stats) } if !dryRun { newNode := NewNetworkDeviceNode(&evt.NetworkFlowMonitor.Device, generationType) - newNode.insertNetworkFlowMonitorEvent(&evt.NetworkFlowMonitor, evt, dryRun, evt.Rules, generationType, imageTag, stats) + newNode.insertNetworkFlowMonitorEvent(&evt.NetworkFlowMonitor, evt, dryRun, evt.Rules, generationType, imageTagID, stats) pn.NetworkDevices[evt.NetworkFlowMonitor.Device] = newNode } return true } // InsertBindEvent inserts a bind event in a process node -func (pn *ProcessNode) InsertBindEvent(evt *model.Event, imageTag string, generationType NodeGenerationType, stats *Stats, dryRun bool) bool { +func (pn *ProcessNode) InsertBindEvent(evt *model.Event, imageTagID uint64, generationType NodeGenerationType, stats *Stats, dryRun bool) bool { if evt.Bind.SyscallEvent.Retval != 0 { return false } @@ -396,7 +396,7 @@ func (pn *ProcessNode) InsertBindEvent(evt *model.Event, imageTag string, genera } // Insert bind event - if sock.InsertBindEvent(&evt.Bind, evt, imageTag, generationType, evt.Rules, dryRun) { + if sock.InsertBindEvent(&evt.Bind, evt, imageTagID, generationType, evt.Rules, dryRun) { newNode = true } @@ -404,7 +404,7 @@ func (pn *ProcessNode) InsertBindEvent(evt *model.Event, imageTag string, genera } // InsertCapabilitiesUsageEvent inserts a capabilities usage event in a process node -func (pn *ProcessNode) InsertCapabilitiesUsageEvent(evt *model.Event, imageTag string, stats *Stats, dryRun bool) bool { +func (pn *ProcessNode) InsertCapabilitiesUsageEvent(evt *model.Event, imageTagID uint64, stats *Stats, dryRun bool) bool { hasNewCapabilitiesUsage := false nextCapability: for capability := uint64(0); capability <= unix.CAP_LAST_CAP; capability++ { @@ -416,7 +416,7 @@ nextCapability: for _, existingCapabilityNode := range pn.Capabilities { if existingCapabilityNode.Capability == capability && existingCapabilityNode.Capable == capable { - existingCapabilityNode.AppendImageTag(imageTag, evt.ResolveEventTime()) + existingCapabilityNode.AppendImageTagID(imageTagID, evt.ResolveEventTime()) continue nextCapability } } @@ -426,7 +426,7 @@ nextCapability: break } - capabilityNode := NewCapabilityNode(capability, capable, evt.ResolveEventTime(), imageTag, Runtime) + capabilityNode := NewCapabilityNode(capability, capable, evt.ResolveEventTime(), imageTagID, Runtime) pn.Capabilities = append(pn.Capabilities, capabilityNode) stats.CapabilityNodes++ } @@ -434,93 +434,93 @@ nextCapability: return hasNewCapabilitiesUsage } -func (pn *ProcessNode) applyImageTagOnLineageIfNeeded(imageTag string) { - if pn.HasImageTag(imageTag) { +func (pn *ProcessNode) applyImageTagOnLineageIfNeeded(imageTagID uint64) { + if pn.HasImageTag(imageTagID) { return } - pn.AppendImageTag(imageTag, pn.Process.ExecTime) + pn.AppendImageTagID(imageTagID, pn.Process.ExecTime) parent := pn.GetParent() for parent != nil { - parent.AppendImageTag(imageTag, pn.Process.ExecTime) + parent.AppendImageTagID(imageTagID, pn.Process.ExecTime) parent = parent.GetParent() } } // TagAllNodes tags this process, its files/dns/socks and childrens with the given image tag -func (pn *ProcessNode) TagAllNodes(imageTag string, timestamp time.Time) { - if imageTag == "" { +func (pn *ProcessNode) TagAllNodes(imageTagID uint64, timestamp time.Time) { + if imageTagID == 0 { return } - pn.AppendImageTag(imageTag, timestamp) + pn.AppendImageTagID(imageTagID, timestamp) for _, file := range pn.Files { - file.tagAllNodes(imageTag, timestamp) + file.tagAllNodes(imageTagID, timestamp) } for _, dns := range pn.DNSNames { - dns.AppendImageTag(imageTag, timestamp) + dns.AppendImageTagID(imageTagID, timestamp) } for _, sock := range pn.Sockets { - sock.AppendImageTag(imageTag, timestamp) + sock.AppendImageTagID(imageTagID, timestamp) } for _, scall := range pn.Syscalls { - scall.AppendImageTag(imageTag, timestamp) + scall.AppendImageTagID(imageTagID, timestamp) } for _, imds := range pn.IMDSEvents { - imds.AppendImageTag(imageTag, timestamp) + imds.AppendImageTagID(imageTagID, timestamp) } for _, device := range pn.NetworkDevices { - device.appendImageTag(imageTag, timestamp) + device.appendImageTag(imageTagID, timestamp) } for _, capabilityNode := range pn.Capabilities { - capabilityNode.AppendImageTag(imageTag, timestamp) + capabilityNode.AppendImageTagID(imageTagID, timestamp) } for _, child := range pn.Children { - child.TagAllNodes(imageTag, timestamp) + child.TagAllNodes(imageTagID, timestamp) } } // EvictImageTag will remove every trace of this image tag, and returns true if the process node should be removed // also, recompute the list of dnsnames and syscalls -func (pn *ProcessNode) EvictImageTag(imageTag string, DNSNames *utils.StringKeys, SyscallsMask map[int]int) bool { - if !pn.HasImageTag(imageTag) { +func (pn *ProcessNode) EvictImageTag(imageTagID uint64, DNSNames *utils.StringKeys, SyscallsMask map[int]int) bool { + if !pn.HasImageTag(imageTagID) { return false // this node doesn't have the tag, and all its children/files/dns/etc shouldn't have it either } - IsNodeEmpty := pn.NodeBase.EvictImageTag(imageTag) + IsNodeEmpty := pn.NodeBase.EvictImageTag(imageTagID) if IsNodeEmpty { // if we removed the last tag, remove entirely the process node from the tree return true } for filename, file := range pn.Files { - if shouldRemoveNode := file.evictImageTag(imageTag); shouldRemoveNode { + if shouldRemoveNode := file.evictImageTag(imageTagID); shouldRemoveNode { delete(pn.Files, filename) } } // Evict image tag from dns nodes for question, dns := range pn.DNSNames { - if shouldRemoveNode := dns.evictImageTag(imageTag, DNSNames); shouldRemoveNode { + if shouldRemoveNode := dns.evictImageTag(imageTagID, DNSNames); shouldRemoveNode { delete(pn.DNSNames, question) } } // Evict image tag from IMDS nodes for key, imds := range pn.IMDSEvents { - if shouldRemoveNode := imds.EvictImageTag(imageTag); shouldRemoveNode { + if shouldRemoveNode := imds.EvictImageTag(imageTagID); shouldRemoveNode { delete(pn.IMDSEvents, key) } } // Evict image tag from network device nodes for key, device := range pn.NetworkDevices { - if shouldRemoveNode := device.evictImageTag(imageTag); shouldRemoveNode { + if shouldRemoveNode := device.evictImageTag(imageTagID); shouldRemoveNode { delete(pn.NetworkDevices, key) } } newSockets := []*SocketNode{} for _, sock := range pn.Sockets { - if shouldRemoveNode := sock.evictImageTag(imageTag); !shouldRemoveNode { + if shouldRemoveNode := sock.evictImageTag(imageTagID); !shouldRemoveNode { newSockets = append(newSockets, sock) } } @@ -528,7 +528,7 @@ func (pn *ProcessNode) EvictImageTag(imageTag string, DNSNames *utils.StringKeys newSyscalls := []*SyscallNode{} for _, scall := range pn.Syscalls { - if shouldRemove := scall.EvictImageTag(imageTag); !shouldRemove { + if shouldRemove := scall.EvictImageTag(imageTagID); !shouldRemove { newSyscalls = append(newSyscalls, scall) SyscallsMask[scall.Syscall] = scall.Syscall } @@ -537,7 +537,7 @@ func (pn *ProcessNode) EvictImageTag(imageTag string, DNSNames *utils.StringKeys var newCapabilities []*CapabilityNode for _, capabilityNode := range pn.Capabilities { - if shouldRemove := capabilityNode.EvictImageTag(imageTag); !shouldRemove { + if shouldRemove := capabilityNode.EvictImageTag(imageTagID); !shouldRemove { newCapabilities = append(newCapabilities, capabilityNode) } } @@ -545,7 +545,7 @@ func (pn *ProcessNode) EvictImageTag(imageTag string, DNSNames *utils.StringKeys newChildren := []*ProcessNode{} for _, child := range pn.Children { - if shouldRemoveNode := child.EvictImageTag(imageTag, DNSNames, SyscallsMask); !shouldRemoveNode { + if shouldRemoveNode := child.EvictImageTag(imageTagID, DNSNames, SyscallsMask); !shouldRemoveNode { newChildren = append(newChildren, child) } } @@ -555,7 +555,8 @@ func (pn *ProcessNode) EvictImageTag(imageTag string, DNSNames *utils.StringKeys // EvictUnusedNodes evicts all child nodes that haven't been touched since the given timestamp // and returns the total number of process nodes evicted, a node is only evicted if all its children are evictable. -func (pn *ProcessNode) EvictUnusedNodes(before time.Time, filepathsInProcessCache map[ImageProcessKey]bool, profileImageName, profileImageTag string) int { +// profileImageTagID is the pre-resolved internal ID for the profile's image tag (0 means unknown/no tag). +func (pn *ProcessNode) EvictUnusedNodes(before time.Time, filepathsInProcessCache map[ImageProcessKey]bool, profileImageName string, profileImageTag string, profileImageTagID uint64) int { totalEvicted := 0 key := ImageProcessKey{ @@ -566,11 +567,11 @@ func (pn *ProcessNode) EvictUnusedNodes(before time.Time, filepathsInProcessCach // First, recursively evict unused nodes from children for i := len(pn.Children) - 1; i >= 0; i-- { child := pn.Children[i] - evicted := child.EvictUnusedNodes(before, filepathsInProcessCache, profileImageName, profileImageTag) + evicted := child.EvictUnusedNodes(before, filepathsInProcessCache, profileImageName, profileImageTag, profileImageTagID) totalEvicted += evicted // If the child process node itself has no image tags left after eviction, remove it entirely - if len(child.Seen) == 0 { + if child.SeenIsEmpty() { pn.Children = append(pn.Children[:i], pn.Children[i+1:]...) totalEvicted++ } @@ -582,17 +583,17 @@ func (pn *ProcessNode) EvictUnusedNodes(before time.Time, filepathsInProcessCach // Edge case: foo->bar->foo, if the second foo is no longer in the process cache, it will still be refreshed because of the first foo key.Filepath = pn.Process.FileEvent.PathnameStr - if filepathsInProcessCache[key] { + if filepathsInProcessCache[key] && profileImageTagID != 0 { // check if the node was supposed to be removed, then update the last seen to now - if pn.Seen[key.ImageTag] != nil && pn.Seen[key.ImageTag].LastSeen.Before(before) { - pn.NodeBase.AppendImageTag(key.ImageTag, time.Now()) + if elem, ok := pn.GetSeenTimes(profileImageTagID); ok && elem.LastSeen.Before(before) { + pn.NodeBase.AppendImageTagID(profileImageTagID, time.Now()) } } _ = pn.NodeBase.EvictBeforeTimestamp(before) // If the process node itself can be evicted - if len(pn.Children) == 0 && len(pn.Seen) == 0 { + if len(pn.Children) == 0 && pn.SeenIsEmpty() { return totalEvicted // No need to evict the activity nodes, since this process node will be removed entirely @@ -602,7 +603,7 @@ func (pn *ProcessNode) EvictUnusedNodes(before time.Time, filepathsInProcessCach for i := len(pn.Syscalls) - 1; i >= 0; i-- { syscallNode := pn.Syscalls[i] if syscallNode.NodeBase.EvictBeforeTimestamp(before) > 0 { - if len(syscallNode.Seen) == 0 { + if syscallNode.SeenIsEmpty() { pn.Syscalls = append(pn.Syscalls[:i], pn.Syscalls[i+1:]...) } } @@ -611,7 +612,7 @@ func (pn *ProcessNode) EvictUnusedNodes(before time.Time, filepathsInProcessCach // Evict unused file nodes for path, fileNode := range pn.Files { if fileNode.NodeBase.EvictBeforeTimestamp(before) > 0 { - if len(fileNode.Seen) == 0 { + if fileNode.SeenIsEmpty() { delete(pn.Files, path) } } @@ -620,7 +621,7 @@ func (pn *ProcessNode) EvictUnusedNodes(before time.Time, filepathsInProcessCach // Evict unused DNS nodes for name, dnsNode := range pn.DNSNames { if dnsNode.NodeBase.EvictBeforeTimestamp(before) > 0 { - if len(dnsNode.Seen) == 0 { + if dnsNode.SeenIsEmpty() { delete(pn.DNSNames, name) } } @@ -629,7 +630,7 @@ func (pn *ProcessNode) EvictUnusedNodes(before time.Time, filepathsInProcessCach // Evict unused IMDS nodes for event, imdsNode := range pn.IMDSEvents { if imdsNode.NodeBase.EvictBeforeTimestamp(before) > 0 { - if len(imdsNode.Seen) == 0 { + if imdsNode.SeenIsEmpty() { delete(pn.IMDSEvents, event) } } @@ -641,7 +642,7 @@ func (pn *ProcessNode) EvictUnusedNodes(before time.Time, filepathsInProcessCach for i := len(pn.Sockets) - 1; i >= 0; i-- { socketNode := pn.Sockets[i] if socketNode.NodeBase.EvictBeforeTimestamp(before) > 0 { - if len(socketNode.Seen) == 0 { + if socketNode.SeenIsEmpty() { pn.Sockets = append(pn.Sockets[:i], pn.Sockets[i+1:]...) } } @@ -651,7 +652,7 @@ func (pn *ProcessNode) EvictUnusedNodes(before time.Time, filepathsInProcessCach for i := len(pn.Capabilities) - 1; i >= 0; i-- { capabilityNode := pn.Capabilities[i] if capabilityNode.NodeBase.EvictBeforeTimestamp(before) > 0 { - if len(capabilityNode.Seen) == 0 { + if capabilityNode.SeenIsEmpty() { pn.Capabilities = append(pn.Capabilities[:i], pn.Capabilities[i+1:]...) } } diff --git a/pkg/security/security_profile/activity_tree/process_node_snapshot.go b/pkg/security/security_profile/activity_tree/process_node_snapshot.go index 92e213f69ca0..2121f14c7f5e 100644 --- a/pkg/security/security_profile/activity_tree/process_node_snapshot.go +++ b/pkg/security/security_profile/activity_tree/process_node_snapshot.go @@ -181,7 +181,7 @@ func (pn *ProcessNode) addFiles(files []string, stats *Stats, newEvent func() *m // TODO: add open flags by parsing `/proc/[pid]/fdinfo/fd` + O_RDONLY|O_CLOEXEC for the shared libs - _ = pn.InsertFileEvent(&evt.Open.File, evt, "", Snapshot, stats, false, reducer, nil) + _ = pn.InsertFileEvent(&evt.Open.File, evt, 0, Snapshot, stats, false, reducer, nil) } } @@ -228,5 +228,5 @@ func (pn *ProcessNode) insertSnapshottedSocket(family uint16, ip net.IP, protoco } evt.Bind.Addr.Port = port - _ = pn.InsertBindEvent(evt, "", Snapshot, stats, false) + _ = pn.InsertBindEvent(evt, 0, Snapshot, stats, false) } diff --git a/pkg/security/security_profile/activity_tree/socket_node.go b/pkg/security/security_profile/activity_tree/socket_node.go index 8f659f0881ba..5c78f33fc966 100644 --- a/pkg/security/security_profile/activity_tree/socket_node.go +++ b/pkg/security/security_profile/activity_tree/socket_node.go @@ -43,10 +43,10 @@ func (sn *SocketNode) Matches(toMatch *SocketNode) bool { return sn.Family == toMatch.Family } -func (sn *SocketNode) evictImageTag(imageTag string) bool { +func (sn *SocketNode) evictImageTag(imageTagID uint64) bool { newBind := []*BindNode{} for _, bind := range sn.Bind { - if shouldRemoveNode := bind.EvictImageTag(imageTag); !shouldRemoveNode { + if shouldRemoveNode := bind.EvictImageTag(imageTagID); !shouldRemoveNode { newBind = append(newBind, bind) } } @@ -58,17 +58,17 @@ func (sn *SocketNode) evictImageTag(imageTag string) bool { } // InsertBindEvent inserts a bind even inside a socket node -func (sn *SocketNode) InsertBindEvent(evt *model.BindEvent, event *model.Event, imageTag string, generationType NodeGenerationType, rules []*model.MatchedRule, dryRun bool) bool { +func (sn *SocketNode) InsertBindEvent(evt *model.BindEvent, event *model.Event, imageTagID uint64, generationType NodeGenerationType, rules []*model.MatchedRule, dryRun bool) bool { evtIP := utils.GetIPStringFromIPNet(evt.Addr.IPNet) for _, n := range sn.Bind { if evt.Addr.Port == n.Port && evtIP == n.IP && evt.Protocol == n.Protocol { if !dryRun { n.MatchedRules = model.AppendMatchedRule(n.MatchedRules, rules) } - if imageTag == "" || n.HasImageTag(imageTag) { + if imageTagID == 0 || n.HasImageTag(imageTagID) { return false } - n.AppendImageTag(imageTag, event.ResolveEventTime()) + n.AppendImageTagID(imageTagID, event.ResolveEventTime()) return false } } @@ -84,7 +84,7 @@ func (sn *SocketNode) InsertBindEvent(evt *model.BindEvent, event *model.Event, } node.NodeBase = NewNodeBase() - node.AppendImageTag(imageTag, event.ResolveEventTime()) + node.AppendImageTagID(imageTagID, event.ResolveEventTime()) sn.Bind = append(sn.Bind, node) } return true diff --git a/pkg/security/security_profile/activity_tree/syscalls_node.go b/pkg/security/security_profile/activity_tree/syscalls_node.go index 98075ce13290..28713cb17d45 100644 --- a/pkg/security/security_profile/activity_tree/syscalls_node.go +++ b/pkg/security/security_profile/activity_tree/syscalls_node.go @@ -20,12 +20,12 @@ type SyscallNode struct { } // NewSyscallNode returns a new SyscallNode instance -func NewSyscallNode(syscall int, timestamp time.Time, imageTag string, generationType NodeGenerationType) *SyscallNode { +func NewSyscallNode(syscall int, timestamp time.Time, imageTagID uint64, generationType NodeGenerationType) *SyscallNode { node := &SyscallNode{ Syscall: syscall, GenerationType: generationType, } node.NodeBase = NewNodeBase() - node.AppendImageTag(imageTag, timestamp) + node.AppendImageTagID(imageTagID, timestamp) return node } diff --git a/pkg/security/security_profile/manager.go b/pkg/security/security_profile/manager.go index 03ac76cc48eb..674306824ef9 100644 --- a/pkg/security/security_profile/manager.go +++ b/pkg/security/security_profile/manager.go @@ -697,7 +697,6 @@ func (m *Manager) evictUnusedNodes() { func (m *Manager) GetNodesInProcessCache() map[activity_tree.ImageProcessKey]bool { cgr := m.resolvers.CGroupResolver - pr := m.resolvers.ProcessResolver tagsResolver := m.resolvers.TagsResolver type imageTagKey struct { @@ -744,7 +743,7 @@ func (m *Manager) GetNodesInProcessCache() map[activity_tree.ImageProcessKey]boo }) // we do the resolution of filepaths here so that we can release the cgroup resolver lock before acquiring the process resolver lock - pr.Walk(func(pce *model.ProcessCacheEntry) { + m.resolvers.ProcessResolver.Walk(func(pce *model.ProcessCacheEntry) { if k, ok := pidToImageTag[pce.Pid]; ok { result[activity_tree.ImageProcessKey{ ImageName: k.imageName, diff --git a/pkg/security/tests/dentry_test.go b/pkg/security/tests/dentry_test.go index a50cf608c1dd..dab3119869d1 100644 --- a/pkg/security/tests/dentry_test.go +++ b/pkg/security/tests/dentry_test.go @@ -23,6 +23,7 @@ import ( "github.com/DataDog/datadog-agent/pkg/security/resolvers/dentry" "github.com/DataDog/datadog-agent/pkg/security/secl/model" "github.com/DataDog/datadog-agent/pkg/security/secl/rules" + "github.com/DataDog/datadog-agent/pkg/security/utils" ) func TestDentryPathERPC(t *testing.T) { @@ -332,7 +333,7 @@ func BenchmarkERPCDentryResolutionPath(b *testing.B) { } // create a new dentry resolver to avoid concurrent map access errors - resolver, err := dentry.NewResolver(test.probe.Config.Probe, test.probe.StatsdClient, p.Erpc) + resolver, err := dentry.NewResolver(test.probe.Config.Probe, test.probe.StatsdClient, p.Erpc, utils.NewLRUStringInterner(16384, "basename")) if err != nil { b.Fatal(err) } @@ -401,7 +402,7 @@ func BenchmarkMapDentryResolutionSegment(b *testing.B) { } // create a new dentry resolver to avoid concurrent map access errors - resolver, err := dentry.NewResolver(test.probe.Config.Probe, test.probe.StatsdClient, p.Erpc) + resolver, err := dentry.NewResolver(test.probe.Config.Probe, test.probe.StatsdClient, p.Erpc, utils.NewLRUStringInterner(16384, "basename")) if err != nil { b.Fatal(err) } @@ -469,7 +470,7 @@ func BenchmarkMapDentryResolutionPath(b *testing.B) { } // create a new dentry resolver to avoid concurrent map access errors - resolver, err := dentry.NewResolver(test.probe.Config.Probe, test.probe.StatsdClient, p.Erpc) + resolver, err := dentry.NewResolver(test.probe.Config.Probe, test.probe.StatsdClient, p.Erpc, utils.NewLRUStringInterner(16384, "basename")) if err != nil { b.Fatal(err) } diff --git a/pkg/security/utils/lru_interner.go b/pkg/security/utils/lru_interner.go index 6a9366449776..ee2083cd37bd 100644 --- a/pkg/security/utils/lru_interner.go +++ b/pkg/security/utils/lru_interner.go @@ -8,25 +8,35 @@ package utils import ( "sync" + "github.com/DataDog/datadog-go/v5/statsd" "github.com/hashicorp/golang-lru/v2/simplelru" + "go.uber.org/atomic" ) // LRUStringInterner is a best-effort LRU-based string deduplicator type LRUStringInterner struct { sync.Mutex store *simplelru.LRU[string, string] + name string + + hits *atomic.Int64 + misses *atomic.Int64 } // NewLRUStringInterner returns a new LRUStringInterner, with the cache size provided -// if the cache size is negative this function will panic -func NewLRUStringInterner(size int) *LRUStringInterner { +// if the cache size is negative this function will panic. The name is used to tag +// metrics emitted by SendStats. +func NewLRUStringInterner(size int, name string) *LRUStringInterner { store, err := simplelru.NewLRU[string, string](size, nil) if err != nil { panic(err) } return &LRUStringInterner{ - store: store, + store: store, + name: name, + hits: atomic.NewInt64(0), + misses: atomic.NewInt64(0), } } @@ -40,9 +50,11 @@ func (si *LRUStringInterner) Deduplicate(value string) string { func (si *LRUStringInterner) deduplicateUnsafe(value string) string { if res, ok := si.store.Get(value); ok { + si.hits.Inc() return res } + si.misses.Inc() si.store.Add(value, value) return value } @@ -56,3 +68,25 @@ func (si *LRUStringInterner) DeduplicateSlice(values []string) { values[i] = si.deduplicateUnsafe(values[i]) } } + +// SendStats sends interner metrics (hits, misses, size) tagged with the interner name. +func (si *LRUStringInterner) SendStats(client statsd.ClientInterface, hitsMetric, missesMetric, sizeMetric string) error { + tags := []string{"interner:" + si.name} + + if hits := si.hits.Swap(0); hits > 0 { + if err := client.Count(hitsMetric, hits, tags, 1.0); err != nil { + return err + } + } + if misses := si.misses.Swap(0); misses > 0 { + if err := client.Count(missesMetric, misses, tags, 1.0); err != nil { + return err + } + } + + si.Lock() + size := si.store.Len() + si.Unlock() + + return client.Gauge(sizeMetric, float64(size), tags, 1.0) +}