diff --git a/pkg/changelog/changelog.go b/pkg/changelog/changelog.go index 6aef4c269112..3f57293e1af8 100644 --- a/pkg/changelog/changelog.go +++ b/pkg/changelog/changelog.go @@ -6,219 +6,202 @@ import ( "github.com/aquasecurity/tracee/pkg/logger" ) -type comparable interface { - ~int | ~float64 | ~string -} - -type item[T comparable] struct { - timestamp time.Time // timestamp of the change - value T // value of the change -} +// +// Entries +// -// The changelog package provides a changelog data structure. It is a list of changes, each with a -// timestamp. The changelog can be queried for the value at a given time. +// MemberKind represents the unique identifier for each kind of entry in the Entries. +// It is used to categorize different kinds of changes tracked by the Entries. +type MemberKind uint8 -// ATTENTION: You should use Changelog within a struct and provide methods to access it, -// coordinating access through your struct mutexes. DO NOT EXPOSE the changelog object directly to -// the outside world as it is not thread-safe. +// MaxEntries represents the maximum number of entries that can be stored for a given kind of entry. +type MaxEntries uint8 -type Changelog[T comparable] struct { - changes []item[T] // list of changes - timestamps map[time.Time]struct{} // set of timestamps (used to avoid duplicates) - maxSize int // maximum amount of changes to keep track of +// entry is an internal structure representing a single change in the Entries. +// It includes the kind of the entry, the timestamp of the change, and the value of the change. +type entry[T comparable] struct { + k MemberKind // Kind of the member, used to categorize the entry. + t time.Time // Timestamp of when the change occurred. + value T // Value of the change. } -// NewChangelog creates a new changelog. -func NewChangelog[T comparable](maxSize int) *Changelog[T] { - return &Changelog[T]{ - changes: []item[T]{}, - timestamps: map[time.Time]struct{}{}, - maxSize: maxSize, - } +// Entries is the main structure that manages a list of changes (entries). +// It keeps track of specifically configured members indicated by MemberKind identifiers. +// When instantiating an Entries struct, one must supply a relevant mapping between the desired +// unique members and the maximum amount of changes that member can track. +// +// ATTENTION: You should use Entries within a struct and provide methods to access it, +// coordinating access through your struct mutexes. DO NOT EXPOSE the Entries object directly to +// the outside world as it is not thread-safe. +type Entries[T comparable] struct { + entryFlags []MaxEntries // Configuration slice defining flags for each member kind. + entries []entry[T] // List of recorded entries. } -// Getters +// NewEntries initializes a new `Entries` structure using the provided flags. +func NewEntries[T comparable](f []MaxEntries) *Entries[T] { + flags := make([]MaxEntries, 0, len(f)) + for _, maxEntries := range f { + if maxEntries == 0 { + logger.Fatalw("maxEntries must be greater than 0") + } -// GetCurrent: Observation on single element changelog. -// -// If there's one element in the changelog, after the loop, left would be set to 1 if the single -// timestamp is before the targetTime, and 0 if it's equal or after. -// -// BEFORE: If the single timestamp is before the targetTime, when we return -// clv.changes[left-1].value, returns clv.changes[0].value, which is the expected behavior. -// -// AFTER: If the single timestamp is equal to, or after the targetTime, the current logic would -// return a "zero" value because of the condition if left == 0. -// -// We need to find the last change that occurred before or exactly at the targetTime. The binary -// search loop finds the position where a new entry with the targetTime timestamp would be inserted -// to maintain chronological order: -// -// This position is stored in "left". -// -// So, to get the last entry that occurred before the targetTime, we need to access the previous -// position, which is left-1. -// -// GetCurrent returns the latest value of the changelog. -func (clv *Changelog[T]) GetCurrent() T { - if len(clv.changes) == 0 { - return returnZero[T]() + flags = append(flags, maxEntries) } - return clv.changes[len(clv.changes)-1].value -} - -// Get returns the value of the changelog at the given time. -func (clv *Changelog[T]) Get(targetTime time.Time) T { - if len(clv.changes) == 0 { - return returnZero[T]() + return &Entries[T]{ + entryFlags: flags, + entries: []entry[T]{}, } +} - idx := clv.findIndex(targetTime) - if idx == 0 { - return returnZero[T]() +// Set adds or updates an entry in the Entries for the specified `MemberKind` ordered by timestamp. +// If the new entry has the same value as the latest one, only the timestamp is updated. +// If there are already the maximum number of entries for this kind, it reuses or replaces an existing entry. +// +// ATTENTION: Make sure to pass a value of the correct type for the specified `MemberKind`. +func (e *Entries[T]) Set(k MemberKind, value T, t time.Time) { + if k >= MemberKind(len(e.entryFlags)) { + logger.Errorw("kind is not present in the entryFlags", "kind", k) } - return clv.changes[idx-1].value -} + maxEntries := e.entryFlags[k] + maxSize := int(maxEntries) + indexes := make([]int, 0) -// GetAll returns all the values of the changelog. -func (clv *Changelog[T]) GetAll() []T { - values := make([]T, 0, len(clv.changes)) - for _, change := range clv.changes { - values = append(values, change.value) + // collect indexes of entries equal to kind + for idx, entry := range e.entries { + if entry.k == k { + indexes = append(indexes, idx) + } } - return values -} -// Setters - -// SetCurrent sets the latest value of the changelog. -func (clv *Changelog[T]) SetCurrent(value T) { - clv.setAt(value, time.Now()) -} + // if there are entries for kind check if the last entry has the same value + if len(indexes) > 0 { + lastIdx := indexes[len(indexes)-1] + if e.entries[lastIdx].value == value && t.After(e.entries[lastIdx].t) { + // only update timestamp and return + e.entries[lastIdx].t = t + return + } + } -// Set sets the value of the changelog at the given time. -func (clv *Changelog[T]) Set(value T, targetTime time.Time) { - clv.setAt(value, targetTime) -} + newEntry := entry[T]{ + k: k, + t: t, + value: value, + } -// private + // + // if there is space, insert the new entry at the correct position + // -// setAt sets the value of the changelog at the given time. -func (clv *Changelog[T]) setAt(value T, targetTime time.Time) { - // If the timestamp is already set, update that value only. - _, ok := clv.timestamps[targetTime] - if ok { - index := clv.findIndex(targetTime) - 1 - if index < 0 { - logger.Debugw("changelog internal error: illegal index for existing timestamp") - } - if !clv.changes[index].timestamp.Equal(targetTime) { // sanity check only (time exists already) - logger.Debugw("changelog internal error: timestamp mismatch") + if len(indexes) < maxSize { + insertPos := e.findInsertIdx(indexes, t) + if insertPos == len(e.entries) { + e.entries = append(e.entries, newEntry) return } - if clv.changes[index].value != value { - logger.Debugw("changelog error: value mismatch for same timestamp") - } - clv.changes[index].value = value - return - } - entry := item[T]{ - timestamp: targetTime, - value: value, + e.insertAt(insertPos, newEntry) + return } - idx := clv.findIndex(entry.timestamp) - // If the changelog has reached its maximum size and the new change would be inserted as the oldest, - // there is no need to add the new change. We can simply return without making any modifications. - if len(clv.changes) >= clv.maxSize && idx == 0 { - return + // + // as there is no space, replace an entry + // + + replaceIdx := indexes[len(indexes)-1] // default index to replace + if t.After(e.entries[replaceIdx].t) { + // reallocate values to the left + e.shiftLeft(indexes) + } else { + // find the correct position to store the entry + replaceIdx = e.findInsertIdx(indexes, t) - 1 + if replaceIdx == -1 { + replaceIdx = 0 + } } - // Insert the new entry in the changelog, keeping the list sorted by timestamp. - clv.changes = append(clv.changes, item[T]{}) - copy(clv.changes[idx+1:], clv.changes[idx:]) - clv.changes[idx] = entry - // Mark the timestamp as set. - clv.timestamps[targetTime] = struct{}{} - - clv.enforceSizeBoundary() + e.entries[replaceIdx] = newEntry } -// findIndex returns the index of the first item in the changelog that is after the given time. -func (clv *Changelog[T]) findIndex(target time.Time) int { - left, right := 0, len(clv.changes) +// Get retrieves the value of the entry for the specified `MemberKind` at or before the given timestamp. +// If no matching entry is found, it returns the default value for the entry type. +func (e *Entries[T]) Get(k MemberKind, timestamp time.Time) T { + for i := len(e.entries) - 1; i >= 0; i-- { + if e.entries[i].k != k { + continue + } - for left < right { - middle := (left + right) / 2 - if clv.changes[middle].timestamp.After(target) { - right = middle - } else { - left = middle + 1 + if e.entries[i].t.Before(timestamp) || e.entries[i].t.Equal(timestamp) { + return e.entries[i].value } } - return left + return getZero[T]() } -// enforceSizeBoundary ensures that the size of the inner array doesn't exceed the limit. -// It applies two methods to reduce the log size to the maximum allowed: -// 1. Unite duplicate values that are trailing one another, removing the oldest of the pair. -// 2. Remove the oldest logs as they are likely less important. - -func (clv *Changelog[T]) enforceSizeBoundary() { - if len(clv.changes) <= clv.maxSize { - // Nothing to do - return +// GetCurrent retrieves the most recent value for the specified `MemberKind`. +// If no entry is found, it returns the default value for the entry type. +func (e *Entries[T]) GetCurrent(k MemberKind) T { + for i := len(e.entries) - 1; i >= 0; i-- { + if e.entries[i].k == k { + return e.entries[i].value + } } - boundaryDiff := len(clv.changes) - clv.maxSize - changed := false - - // Compact the slice in place - writeIdx := 0 - for readIdx := 0; readIdx < len(clv.changes); readIdx++ { - nextIdx := readIdx + 1 - if nextIdx < len(clv.changes) && - clv.changes[nextIdx].value == clv.changes[readIdx].value && - boundaryDiff > 0 { - // Remove the oldest (readIdx) from the duplicate pair - delete(clv.timestamps, clv.changes[readIdx].timestamp) - boundaryDiff-- - changed = true - continue - } + return getZero[T]() +} - // If elements have been removed or moved, update the map and the slice - if changed { - clv.changes[writeIdx] = clv.changes[readIdx] +// GetAll retrieves all values for the specified `MemberKind`, from the newest to the oldest. +func (e *Entries[T]) GetAll(k MemberKind) []T { + values := make([]T, e.Count(k)) + for i := len(e.entries) - 1; i >= 0; i-- { + if e.entries[i].k == k { + values = append(values, e.entries[i].value) } - - writeIdx++ } - if changed { - clear(clv.changes[writeIdx:]) - clv.changes = clv.changes[:writeIdx] + return values +} + +// Count returns the number of entries recorded for the specified `MemberKind`. +func (e *Entries[T]) Count(k MemberKind) int { + count := 0 + for _, entry := range e.entries { + if entry.k == k { + count++ + } } - if len(clv.changes) <= clv.maxSize { - // Size is within limits after compaction - return + return count +} + +// findInsertIdx finds the correct index to insert a new entry based on its timestamp. +func (e *Entries[T]) findInsertIdx(indexes []int, t time.Time) int { + for i := len(indexes) - 1; i >= 0; i-- { + if e.entries[indexes[i]].t.Before(t) { + return indexes[i] + 1 + } } - // As it still exceeds maxSize, remove the oldest entries in the remaining slice - boundaryDiff = len(clv.changes) - clv.maxSize - for i := 0; i < boundaryDiff; i++ { - delete(clv.timestamps, clv.changes[i].timestamp) + return len(indexes) +} + +// insertAt inserts a new entry at the specified index in the entries list. +func (e *Entries[T]) insertAt(idx int, newEntry entry[T]) { + e.entries = append(e.entries[:idx], append([]entry[T]{newEntry}, e.entries[idx:]...)...) +} + +// shiftLeft shifts entries within the given indexes to the left, discarding the oldest entry. +func (e *Entries[T]) shiftLeft(indexes []int) { + for i := 0; i < len(indexes)-1; i++ { + e.entries[indexes[i]] = e.entries[indexes[i+1]] } - clear(clv.changes[:boundaryDiff]) - clv.changes = clv.changes[boundaryDiff:] } -// returnZero returns the zero value of the type T. -func returnZero[T any]() T { +// getZero returns the zero value for the type `T`. +func getZero[T comparable]() T { var zero T return zero } diff --git a/pkg/changelog/changelog_benchmark_test.go b/pkg/changelog/changelog_benchmark_test.go index 2b349d9433e4..a559fdbc0749 100644 --- a/pkg/changelog/changelog_benchmark_test.go +++ b/pkg/changelog/changelog_benchmark_test.go @@ -5,75 +5,6 @@ import ( "time" ) -func Benchmark_enforceSizeBoundary(b *testing.B) { - testCases := []struct { - name string - changelog Changelog[int] - }{ - { - name: "No change needed", - changelog: Changelog[int]{ - changes: []item[int]{ - {value: 1, timestamp: getTimeFromSec(1)}, - {value: 2, timestamp: getTimeFromSec(2)}, - }, - timestamps: map[time.Time]struct{}{ - getTimeFromSec(1): {}, - getTimeFromSec(2): {}, - }, - maxSize: 5, - }, - }, - { - name: "Trim excess with duplicates", - changelog: Changelog[int]{ - changes: []item[int]{ - {value: 1, timestamp: getTimeFromSec(1)}, - {value: 1, timestamp: getTimeFromSec(2)}, - {value: 2, timestamp: getTimeFromSec(3)}, - {value: 3, timestamp: getTimeFromSec(4)}, - {value: 3, timestamp: getTimeFromSec(5)}, - }, - timestamps: map[time.Time]struct{}{ - getTimeFromSec(1): {}, - getTimeFromSec(2): {}, - getTimeFromSec(3): {}, - getTimeFromSec(4): {}, - getTimeFromSec(5): {}, - }, - maxSize: 3, - }, - }, - { - name: "Remove oldest entries", - changelog: Changelog[int]{ - changes: []item[int]{ - {value: 1, timestamp: getTimeFromSec(1)}, - {value: 2, timestamp: getTimeFromSec(2)}, - {value: 3, timestamp: getTimeFromSec(3)}, - {value: 4, timestamp: getTimeFromSec(4)}, - }, - timestamps: map[time.Time]struct{}{ - getTimeFromSec(1): {}, - getTimeFromSec(2): {}, - getTimeFromSec(3): {}, - getTimeFromSec(4): {}, - }, - maxSize: 2, - }, - }, - } - - for _, tc := range testCases { - b.Run(tc.name, func(b *testing.B) { - for i := 0; i < b.N; i++ { - clv := tc.changelog // Create a copy for each iteration - clv.enforceSizeBoundary() - } - }) - } -} - func Benchmark_Set(b *testing.B) { // Test cases where the Changelog needs to enforce the size boundary testCasesAllScenarios := []struct { @@ -94,61 +25,65 @@ func Benchmark_Set(b *testing.B) { }, { value: 672, - time: getTimeFromSec(3), // will trigger removal of oldest entry + time: getTimeFromSec(3), }, { value: 642, - time: getTimeFromSec(4), // will trigger coalescing of duplicate values + time: getTimeFromSec(4), }, { value: 672, - time: getTimeFromSec(5), // will trigger coalescing of duplicate values + time: getTimeFromSec(5), }, { value: 6642, - time: getTimeFromSec(6), // will trigger removal of oldest entry + time: getTimeFromSec(6), }, { value: 672, - time: getTimeFromSec(7), // will trigger coalescing of duplicate values + time: getTimeFromSec(7), }, { value: 642, - time: getTimeFromSec(8), // will trigger coalescing of duplicate values + time: getTimeFromSec(8), }, { value: 6672, - time: getTimeFromSec(9), // will trigger removal of oldest entry + time: getTimeFromSec(9), }, { value: 9642, - time: getTimeFromSec(10), // will trigger removal of oldest entry + time: getTimeFromSec(10), }, { value: 0, - time: getTimeFromSec(0), // will just update the value + time: getTimeFromSec(0), }, { value: 0, - time: getTimeFromSec(1), // will just update the value + time: getTimeFromSec(1), }, { value: 0, - time: getTimeFromSec(2), // will just update the value + time: getTimeFromSec(2), }, { value: 0, - time: getTimeFromSec(3), // will just update the value + time: getTimeFromSec(3), }, } + entryFlagsAllScenarios := []MaxEntries{ + testInt0: 3, + } + b.Run("All Scenarios", func(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() - clv := NewChangelog[int](3) + clv := NewEntries[int](entryFlagsAllScenarios) b.StartTimer() for _, tc := range testCasesAllScenarios { - clv.Set(tc.value, tc.time) + clv.Set(testInt0, tc.value, tc.time) } } }) @@ -220,13 +155,17 @@ func Benchmark_Set(b *testing.B) { }, } + entryFlagsWithinLimit := []MaxEntries{ + testInt0: 15, + } + b.Run("Within Limit", func(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() - clv := NewChangelog[int](15) + clv := NewEntries[int](entryFlagsWithinLimit) b.StartTimer() for _, tc := range testCasesWithinLimit { - clv.Set(tc.value, tc.time) + clv.Set(testInt0, tc.value, tc.time) } } }) diff --git a/pkg/changelog/changelog_test.go b/pkg/changelog/changelog_test.go index f831aa7e5231..68b8534d3644 100644 --- a/pkg/changelog/changelog_test.go +++ b/pkg/changelog/changelog_test.go @@ -1,350 +1,154 @@ package changelog import ( - "reflect" "testing" "time" "github.com/stretchr/testify/assert" ) -func TestChangelog(t *testing.T) { - t.Parallel() - - t.Run("GetCurrent on an empty changelog", func(t *testing.T) { - cl := NewChangelog[int](3) +const ( + // int members + testInt0 MemberKind = iota + testInt1 + testInt2 +) - // Test GetCurrent on an empty changelog - assert.Zero(t, cl.GetCurrent()) - }) - - t.Run("Set and get", func(t *testing.T) { - cl := NewChangelog[int](3) - testVal := 42 - - cl.SetCurrent(testVal) - assert.Equal(t, testVal, cl.GetCurrent()) - }) - - t.Run("Set and get on set time", func(t *testing.T) { - cl := NewChangelog[int](3) - testVal1 := 42 - testVal2 := 76 - testVal3 := 76 - - // Test with 3 stages of the changelog to make sure the binary search works well for - // different lengths (both odd and even). - now := time.Now() - cl.Set(testVal1, now) - assert.Equal(t, testVal1, cl.Get(now)) - - cl.Set(testVal2, now.Add(time.Second)) - assert.Equal(t, testVal1, cl.Get(now)) - assert.Equal(t, testVal2, cl.Get(now.Add(time.Second))) - - cl.Set(testVal3, now.Add(2*time.Second)) - assert.Equal(t, testVal1, cl.Get(now)) - assert.Equal(t, testVal2, cl.Get(now.Add(time.Second))) - assert.Equal(t, testVal3, cl.Get(now.Add(2*time.Second))) - }) - - t.Run("Set twice on the same time", func(t *testing.T) { - cl := NewChangelog[int](3) - testVal := 42 - - now := time.Now() - cl.Set(testVal, now) - cl.Set(testVal, now) - assert.Equal(t, testVal, cl.Get(now)) - assert.Len(t, cl.GetAll(), 1) - assert.Equal(t, testVal, cl.Get(now)) - }) - - t.Run("Get on an empty changelog", func(t *testing.T) { - cl := NewChangelog[int](3) - - assert.Zero(t, cl.GetCurrent()) - }) - - t.Run("Test 1 second interval among changes", func(t *testing.T) { - cl := NewChangelog[int](3) - - cl.SetCurrent(1) - time.Sleep(2 * time.Second) - cl.SetCurrent(2) - time.Sleep(2 * time.Second) - cl.SetCurrent(3) - - now := time.Now() - - assert.Equal(t, 1, cl.Get(now.Add(-4*time.Second))) - assert.Equal(t, 2, cl.Get(now.Add(-2*time.Second))) - assert.Equal(t, 3, cl.Get(now)) - }) - - t.Run("Test 100 milliseconds interval among changes", func(t *testing.T) { - cl := NewChangelog[int](3) - - cl.SetCurrent(1) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(2) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(3) - - now := time.Now() - - assert.Equal(t, 1, cl.Get(now.Add(-200*time.Millisecond))) - assert.Equal(t, 2, cl.Get(now.Add(-100*time.Millisecond))) - assert.Equal(t, 3, cl.Get(now)) - }) - - t.Run("Test getting all values at once", func(t *testing.T) { - cl := NewChangelog[int](3) - - cl.SetCurrent(1) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(2) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(3) - - expected := []int{1, 2, 3} - assert.Equal(t, expected, cl.GetAll()) - }) - - t.Run("Pass max size wit repeated values", func(t *testing.T) { - cl := NewChangelog[int](3) - - cl.SetCurrent(1) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(2) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(2) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(3) - - now := time.Now() - assert.Equal(t, 1, cl.Get(now.Add(-300*time.Millisecond))) - assert.Equal(t, 1, cl.Get(now.Add(-200*time.Millisecond))) // oldest 2 is removed, so 1 is returned - assert.Equal(t, 2, cl.Get(now.Add(-100*time.Millisecond))) - assert.Equal(t, 3, cl.Get(now)) - assert.Len(t, cl.GetAll(), 3) - }) - - t.Run("Pass max size with unique values", func(t *testing.T) { - cl := NewChangelog[int](3) - - cl.SetCurrent(1) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(2) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(3) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(4) - - now := time.Now() - assert.Equal(t, 0, cl.Get(now.Add(-300*time.Millisecond))) - assert.Equal(t, 2, cl.Get(now.Add(-200*time.Millisecond))) - assert.Equal(t, 3, cl.Get(now.Add(-100*time.Millisecond))) - assert.Equal(t, 4, cl.Get(now.Add(time.Millisecond))) - assert.Len(t, cl.GetAll(), 3) - }) - - t.Run("Pass max size with new old value", func(t *testing.T) { - cl := NewChangelog[int](3) - - cl.SetCurrent(1) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(2) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(3) - - now := time.Now() - cl.Set(4, now.Add(-400*time.Millisecond)) - - // Make sure that the new value was not added - assert.Equal(t, 0, cl.Get(now.Add(-300*time.Millisecond))) - - // Sanity check - assert.Equal(t, 1, cl.Get(now.Add(-200*time.Millisecond))) - assert.Equal(t, 2, cl.Get(now.Add(-100*time.Millisecond))) - assert.Equal(t, 3, cl.Get(now)) - assert.Len(t, cl.GetAll(), 3) - }) - - t.Run("Zero sized changelog", func(t *testing.T) { - cl := NewChangelog[int](0) - - cl.SetCurrent(1) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(2) - time.Sleep(100 * time.Millisecond) - cl.SetCurrent(3) - - now := time.Now() - cl.Set(4, now.Add(-400*time.Millisecond)) - - // Make sure that the new value was not added - - // Sanity check - assert.Equal(t, 0, cl.Get(now.Add(-300*time.Millisecond))) - assert.Equal(t, 0, cl.Get(now.Add(-200*time.Millisecond))) - assert.Equal(t, 0, cl.Get(now.Add(-100*time.Millisecond))) - assert.Equal(t, 0, cl.Get(now)) - assert.Empty(t, cl.GetAll()) - }) - - t.Run("Test enforceSizeBoundary", func(t *testing.T) { - type TestCase struct { - name string - maxSize int - initialChanges []item[int] - expectedChanges []item[int] - expectedTimestamps map[time.Time]struct{} - } - - testCases := []TestCase{ - { - name: "No Action Required", - maxSize: 3, - initialChanges: []item[int]{ - {timestamp: getTimeFromSec(42), value: 1}, - {timestamp: getTimeFromSec(43), value: 2}, - {timestamp: getTimeFromSec(44), value: 3}, - }, - expectedChanges: []item[int]{ - {timestamp: getTimeFromSec(42), value: 1}, - {timestamp: getTimeFromSec(43), value: 2}, - {timestamp: getTimeFromSec(44), value: 3}, - }, - expectedTimestamps: map[time.Time]struct{}{ - getTimeFromSec(42): {}, - getTimeFromSec(43): {}, - getTimeFromSec(44): {}, - }, - }, - { - name: "Basic Removal of Oldest Entries", - maxSize: 3, - initialChanges: []item[int]{ - {timestamp: getTimeFromSec(42), value: 1}, - {timestamp: getTimeFromSec(43), value: 2}, - {timestamp: getTimeFromSec(44), value: 3}, - {timestamp: getTimeFromSec(45), value: 4}, - }, - expectedChanges: []item[int]{ - {timestamp: getTimeFromSec(43), value: 2}, - {timestamp: getTimeFromSec(44), value: 3}, - {timestamp: getTimeFromSec(45), value: 4}, - }, - expectedTimestamps: map[time.Time]struct{}{ - getTimeFromSec(43): {}, - getTimeFromSec(44): {}, - getTimeFromSec(45): {}, - }, - }, - { - name: "Compacting Duplicate Values - Start", - maxSize: 3, - initialChanges: []item[int]{ - {timestamp: getTimeFromSec(42), value: 1}, - {timestamp: getTimeFromSec(43), value: 1}, - {timestamp: getTimeFromSec(44), value: 2}, - {timestamp: getTimeFromSec(45), value: 3}, - }, - expectedChanges: []item[int]{ - {timestamp: getTimeFromSec(43), value: 1}, - {timestamp: getTimeFromSec(44), value: 2}, - {timestamp: getTimeFromSec(45), value: 3}, - }, - expectedTimestamps: map[time.Time]struct{}{ - getTimeFromSec(43): {}, - getTimeFromSec(44): {}, - getTimeFromSec(45): {}, - }, - }, - { - name: "Compacting Duplicate Values - Middle", - maxSize: 3, - initialChanges: []item[int]{ - {timestamp: getTimeFromSec(42), value: 1}, - {timestamp: getTimeFromSec(43), value: 2}, - {timestamp: getTimeFromSec(44), value: 2}, - {timestamp: getTimeFromSec(45), value: 3}, - }, - expectedChanges: []item[int]{ - {timestamp: getTimeFromSec(42), value: 1}, - {timestamp: getTimeFromSec(44), value: 2}, - {timestamp: getTimeFromSec(45), value: 3}, - }, - expectedTimestamps: map[time.Time]struct{}{ - getTimeFromSec(42): {}, - getTimeFromSec(44): {}, - getTimeFromSec(45): {}, - }, - }, - { - name: "Compacting Duplicate Values - End", - maxSize: 3, - initialChanges: []item[int]{ - {timestamp: getTimeFromSec(42), value: 1}, - {timestamp: getTimeFromSec(43), value: 2}, - {timestamp: getTimeFromSec(44), value: 3}, - {timestamp: getTimeFromSec(45), value: 3}, - }, - expectedChanges: []item[int]{ - {timestamp: getTimeFromSec(42), value: 1}, - {timestamp: getTimeFromSec(43), value: 2}, - {timestamp: getTimeFromSec(45), value: 3}, - }, - expectedTimestamps: map[time.Time]struct{}{ - getTimeFromSec(42): {}, - getTimeFromSec(43): {}, - getTimeFromSec(45): {}, - }, - }, - { - name: "Combination of Compaction and Removal of Oldest Entries", - maxSize: 3, - initialChanges: []item[int]{ - {timestamp: getTimeFromSec(42), value: 1}, - {timestamp: getTimeFromSec(43), value: 2}, - {timestamp: getTimeFromSec(44), value: 2}, - {timestamp: getTimeFromSec(45), value: 2}, - {timestamp: getTimeFromSec(46), value: 3}, - {timestamp: getTimeFromSec(47), value: 4}, - }, - expectedChanges: []item[int]{ - {timestamp: getTimeFromSec(45), value: 2}, - {timestamp: getTimeFromSec(46), value: 3}, - {timestamp: getTimeFromSec(47), value: 4}, - }, - expectedTimestamps: map[time.Time]struct{}{ - getTimeFromSec(45): {}, - getTimeFromSec(46): {}, - getTimeFromSec(47): {}, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - cl := NewChangelog[int](tc.maxSize) - for _, change := range tc.initialChanges { - cl.Set(change.value, change.timestamp) - } - - cl.enforceSizeBoundary() - - eq := reflect.DeepEqual(cl.timestamps, tc.expectedTimestamps) - assert.True(t, eq) - - eq = reflect.DeepEqual(cl.changes, tc.expectedChanges) - assert.True(t, eq) - }) - } - }) -} +const ( + // string members + testString MemberKind = iota +) func getTimeFromSec(second int) time.Time { return time.Unix(int64(second), 0) } + +func TestChangelogEntries_GetZeroValue(t *testing.T) { + flags := []MaxEntries{ + testInt0: 1, + } + changelog := NewEntries[int](flags) + time0 := getTimeFromSec(0) + + // Assert zero value before any set + assert.Equal(t, 0, changelog.Get(testInt0, time0), "Expected zero value for testInt0") + assert.Equal(t, 0, changelog.GetCurrent(testInt0), "Expected zero value for testInt0") + + // Set and assert value + changelog.Set(testInt0, 3001, time0) + assert.Equal(t, 3001, changelog.Get(testInt0, time0), "Expected testInt0 to be 3001") + assert.Equal(t, 3001, changelog.GetCurrent(testInt0), "Expected current testInt0 to be 3001") + + // Check the count of entries + assert.Equal(t, 1, changelog.Count(testInt0), "Expected 1 entry") + assert.Equal(t, 0, changelog.Count(testInt1), "Expected 0 entries") +} + +func TestChangelogEntries_ShiftAndReplace(t *testing.T) { + flags := []MaxEntries{ + testString: 2, + } + changelog := NewEntries[string](flags) + + // Set entries and assert initial values + changelog.Set(testString, "initial", getTimeFromSec(0)) + changelog.Set(testString, "updated", getTimeFromSec(1)) + assert.Equal(t, "initial", changelog.Get(testString, getTimeFromSec(0)), "Expected first entry to be 'initial'") + assert.Equal(t, "updated", changelog.Get(testString, getTimeFromSec(1)), "Expected second entry to be 'updated'") + + // Test shifting and replacement + changelog.Set(testString, "final", getTimeFromSec(2)) + assert.Equal(t, "updated", changelog.Get(testString, getTimeFromSec(1)), "Expected oldest entry to be removed") + assert.Equal(t, "final", changelog.Get(testString, getTimeFromSec(2)), "Expected newest entry to be 'final'") + assert.Equal(t, "final", changelog.GetCurrent(testString), "Expected current entry to be 'final'") + + // Check the count of entries + assert.Equal(t, 2, changelog.Count(testString), "Expected 2 entries") +} + +func TestChangelogEntries_ReplaceMostRecentWithSameValue(t *testing.T) { + flags := []MaxEntries{ + testString: 2, + } + changelog := NewEntries[string](flags) + + // Set entries and assert initial value + changelog.Set(testString, "initial", getTimeFromSec(0)) + assert.Equal(t, "initial", changelog.Get(testString, getTimeFromSec(0)), "Expected first entry to be 'initial'") + changelog.Set(testString, "initial", getTimeFromSec(1)) + assert.Equal(t, "initial", changelog.Get(testString, getTimeFromSec(1)), "Expected first entry to have timestamp updated") + + // Test replacement of most recent entry with same value + changelog.Set(testString, "second", getTimeFromSec(2)) + assert.Equal(t, "initial", changelog.Get(testString, getTimeFromSec(1)), "Expected first entry to be 'initial'") + assert.Equal(t, "second", changelog.Get(testString, getTimeFromSec(2)), "Expected second entry to have timestamp updated") + + // Check the count of entries + assert.Equal(t, 2, changelog.Count(testString), "Expected 2 entries") +} + +func TestChangelogEntries_InsertWithOlderTimestamp(t *testing.T) { + flags := []MaxEntries{ + testString: 3, + } + changelog := NewEntries[string](flags) + now := getTimeFromSec(0) + + // Insert entries with increasing timestamps + changelog.Set(testString, "first", now) + changelog.Set(testString, "second", now.Add(1*time.Second)) + changelog.Set(testString, "third", now.Add(2*time.Second)) + + // Insert an entry with an older timestamp + changelog.Set(testString, "older", now.Add(1*time.Millisecond)) + + // Check the count of entries + assert.Equal(t, 3, changelog.Count(testString), "Expected 3 entries") + + // Verify the order of entries + assert.Equal(t, "older", changelog.Get(testString, now.Add(1*time.Millisecond)), "Expected 'older' to be the first entry") + assert.Equal(t, "second", changelog.Get(testString, now.Add(1*time.Second)), "Expected 'second' to be the second entry") + assert.Equal(t, "third", changelog.Get(testString, now.Add(2*time.Second)), "Expected 'third' to be the last entry") + + // Insert an entry with an intermediate timestamp + changelog.Set(testString, "second-third", now.Add(1*time.Second+1*time.Millisecond)) + + // Verify the order of entries + assert.Equal(t, "older", changelog.Get(testString, now.Add(1*time.Millisecond)), "Expected 'older' to be the first entry") + assert.Equal(t, "second-third", changelog.Get(testString, now.Add(1*time.Second+1*time.Millisecond)), "Expected 'second-third' to be the second entry") + assert.Equal(t, "third", changelog.Get(testString, now.Add(2*time.Second)), "Expected 'third' to be the last entry") + + // Check the count of entries + assert.Equal(t, 3, changelog.Count(testString), "Expected 3 entries") +} + +func TestChangelogEntries_InsertSameValueWithNewTimestamp(t *testing.T) { + flags := []MaxEntries{ + testString: 3, + } + changelog := NewEntries[string](flags) + + // Insert entries with increasing timestamps + changelog.Set(testString, "same", getTimeFromSec(0)) + + // Replace the last entry with the same value but a new timestamp + changelog.Set(testString, "same", getTimeFromSec(1)) + + // Verify the order of entries + assert.Equal(t, "same", changelog.Get(testString, getTimeFromSec(1)), "Expected 'same' to be the second entry") + + // Insert entries with sequential timestamps + changelog.Set(testString, "new", getTimeFromSec(2)) + changelog.Set(testString, "other", getTimeFromSec(3)) + + // Replace the last entry with the same value but a new timestamp + changelog.Set(testString, "other", getTimeFromSec(4)) + + // Verify the order of entries + assert.Equal(t, "same", changelog.Get(testString, getTimeFromSec(1)), "Expected 'same' to be the first entry") + assert.Equal(t, "new", changelog.Get(testString, getTimeFromSec(2)), "Expected 'new' to be the second entry") + assert.Equal(t, "other", changelog.Get(testString, getTimeFromSec(4)), "Expected 'other' to be the last entry") + + // Check the count of entries + assert.Equal(t, 3, changelog.Count(testString), "Expected 3 entries") +} diff --git a/pkg/proctree/fileinfo.go b/pkg/proctree/fileinfo.go index 078b8a692711..a9e67fb82b07 100644 --- a/pkg/proctree/fileinfo.go +++ b/pkg/proctree/fileinfo.go @@ -1,10 +1,11 @@ package proctree import ( + "strings" "sync" "time" - ch "github.com/aquasecurity/tracee/pkg/changelog" + "github.com/aquasecurity/tracee/pkg/changelog" ) // FileInfoFeed allows external packages to set/get multiple values of a task at once. @@ -21,243 +22,379 @@ type FileInfoFeed struct { // File Info // +const ( + // string members + fileInfoPath changelog.MemberKind = iota +) + +const ( + // int members + fileInfoDev changelog.MemberKind = iota + fileInfoCtime + fileInfoInode + fileInfoInodeMode +) + +var ( + // fileInfoMutableStringsFlags is a slice with metadata about the mutable string members of a FileInfo. + fileInfoMutableStringsFlags = []changelog.MaxEntries{ + fileInfoPath: 3, // file path + } + + // fileInfoMutableIntsFlags is a slice with metadata about the mutable int members of a FileInfo. + fileInfoMutableIntsFlags = []changelog.MaxEntries{ + fileInfoDev: 3, // device number of the file + fileInfoCtime: 3, // creation time of the file + fileInfoInode: 3, // inode number of the file + fileInfoInodeMode: 3, // inode mode of the file + } +) + // FileInfo represents a file. type FileInfo struct { - path *ch.Changelog[string] // file path - dev *ch.Changelog[int] // device number of the file - ctime *ch.Changelog[int] // creation time of the file - inode *ch.Changelog[int] // inode number of the file - inodeMode *ch.Changelog[int] // inode mode of the file - mutex *sync.RWMutex + mutableStrings *changelog.Entries[string] + mutableInts *changelog.Entries[int] + mutex *sync.RWMutex } // NewFileInfo creates a new file. -func NewFileInfo(maxLogSize int) *FileInfo { +func NewFileInfo() *FileInfo { return &FileInfo{ - path: ch.NewChangelog[string](maxLogSize), - dev: ch.NewChangelog[int](maxLogSize), - ctime: ch.NewChangelog[int](maxLogSize), - inode: ch.NewChangelog[int](maxLogSize), - inodeMode: ch.NewChangelog[int](maxLogSize), - mutex: &sync.RWMutex{}, + mutableStrings: changelog.NewEntries[string](fileInfoMutableStringsFlags), + mutableInts: changelog.NewEntries[int](fileInfoMutableIntsFlags), + mutex: &sync.RWMutex{}, } } // NewFileInfoFeed creates a new file with values from the given feed. -func NewFileInfoFeed(maxLogSize int, feed FileInfoFeed) *FileInfo { - new := NewFileInfo(maxLogSize) - new.SetFeed(feed) +func NewFileInfoFeed(feed FileInfoFeed) *FileInfo { + new := NewFileInfo() + new.setFeed(feed) + return new } +// +// Setters +// + // Multiple values at once (using a feed structure) -// SetFeed sets the values of the file from a feed. +// SetFeed sets the values of the file from a feed at the current time. func (fi *FileInfo) SetFeed(feed FileInfoFeed) { fi.mutex.Lock() defer fi.mutex.Unlock() - fi.SetFeedAt(feed, time.Now()) + + fi.setFeed(feed) } // SetFeedAt sets the values of the file from a feed at the given time. func (fi *FileInfo) SetFeedAt(feed FileInfoFeed, targetTime time.Time) { fi.mutex.Lock() defer fi.mutex.Unlock() - fi.setFeedAt(feed, targetTime) -} - -// Paths theoretically has no limit, but we do need to set a limit for the sake of -// managing memory more responsibly. -const MaxPathLen = 1024 - -func (fi *FileInfo) setFeedAt(feed FileInfoFeed, targetTime time.Time) { - if feed.Path != "" { - filePath := feed.Path - if len(filePath) > MaxPathLen { - // Take only the end of the path, as the specific file name and location are the most - // important parts. - filePath = filePath[len(filePath)-MaxPathLen:] - } - fi.path.Set(filePath, targetTime) - } - if feed.Dev >= 0 { - fi.dev.Set(feed.Dev, targetTime) - } - if feed.Ctime >= 0 { - fi.ctime.Set(feed.Ctime, targetTime) - } - if feed.Inode >= 0 { - fi.inode.Set(feed.Inode, targetTime) - } - if feed.InodeMode >= 0 { - fi.inodeMode.Set(feed.InodeMode, targetTime) - } -} - -// GetFeed returns the values of the file as a feed. -func (fi *FileInfo) GetFeed() FileInfoFeed { - fi.mutex.RLock() - defer fi.mutex.RUnlock() - return fi.getFeedAt(time.Now()) -} - -// GetFeedAt returns the values of the file as a feed at the given time. -func (fi *FileInfo) GetFeedAt(targetTime time.Time) FileInfoFeed { - fi.mutex.RLock() - defer fi.mutex.RUnlock() - return fi.getFeedAt(targetTime) // return values at the given time -} -func (fi *FileInfo) getFeedAt(targetTime time.Time) FileInfoFeed { - return FileInfoFeed{ - Path: fi.path.Get(targetTime), - Dev: fi.dev.Get(targetTime), - Ctime: fi.ctime.Get(targetTime), - Inode: fi.inode.Get(targetTime), - InodeMode: fi.inodeMode.Get(targetTime), - } + fi.setFeedAt(feed, targetTime) } -// Setters +// Single values // SetPath sets the path of the file. func (fi *FileInfo) SetPath(path string) { fi.mutex.Lock() defer fi.mutex.Unlock() - fi.path.Set(path, time.Now()) + + fi.setPathAt(path, time.Now()) } // SetPathAt sets the path of the file at the given time. func (fi *FileInfo) SetPathAt(path string, targetTime time.Time) { fi.mutex.Lock() defer fi.mutex.Unlock() - fi.path.Set(path, targetTime) + + fi.setPathAt(path, targetTime) } // SetDev sets the device number of the file. func (fi *FileInfo) SetDev(dev int) { fi.mutex.Lock() defer fi.mutex.Unlock() - fi.dev.Set(dev, time.Now()) + + fi.setDevAt(dev, time.Now()) } // SetDevAt sets the device number of the file at the given time. func (fi *FileInfo) SetDevAt(dev int, targetTime time.Time) { fi.mutex.Lock() defer fi.mutex.Unlock() - fi.dev.Set(dev, targetTime) + + fi.setDevAt(dev, targetTime) } // SetCtime sets the creation time of the file. func (fi *FileInfo) SetCtime(ctime int) { fi.mutex.Lock() defer fi.mutex.Unlock() - fi.ctime.Set(ctime, time.Now()) + + fi.setCtimeAt(ctime, time.Now()) } // SetCtimeAt sets the creation time of the file at the given time. func (fi *FileInfo) SetCtimeAt(ctime int, targetTime time.Time) { fi.mutex.Lock() defer fi.mutex.Unlock() - fi.ctime.Set(ctime, targetTime) + + fi.setCtimeAt(ctime, targetTime) } // SetInode sets the inode number of the file. func (fi *FileInfo) SetInode(inode int) { fi.mutex.Lock() defer fi.mutex.Unlock() - fi.inode.Set(inode, time.Now()) + + fi.setInodeAt(inode, time.Now()) } // SetInodeAt sets the inode number of the file at the given time. func (fi *FileInfo) SetInodeAt(inode int, targetTime time.Time) { fi.mutex.Lock() defer fi.mutex.Unlock() - fi.inode.Set(inode, targetTime) + + fi.setInodeAt(inode, targetTime) } // SetInodeMode sets the inode mode of the file. func (fi *FileInfo) SetInodeMode(inodeMode int) { fi.mutex.Lock() defer fi.mutex.Unlock() - fi.inodeMode.Set(inodeMode, time.Now()) + + fi.setInodeAt(inodeMode, time.Now()) } // SetInodeModeAt sets the inode mode of the file at the given time. func (fi *FileInfo) SetInodeModeAt(inodeMode int, targetTime time.Time) { fi.mutex.Lock() defer fi.mutex.Unlock() - fi.inodeMode.Set(inodeMode, targetTime) + + fi.setInodeModeAt(inodeMode, targetTime) +} + +// private setters + +func (fi *FileInfo) setFeed(feed FileInfoFeed) { + fi.setFeedAt(feed, time.Now()) +} + +// Paths theoretically has no limit, but we do need to set a limit for the sake of +// managing memory more responsibly. +const MaxPathLen = 1024 + +func (fi *FileInfo) setFeedAt(feed FileInfoFeed, targetTime time.Time) { + if feed.Path != "" { + filePath := feed.Path + if len(filePath) > MaxPathLen { + // Take only the end of the path, as the specific file name and location + // are the most important parts. Cloning prevents memory retention. + filePath = strings.Clone(filePath[len(filePath)-MaxPathLen:]) + } + fi.setPathAt(filePath, targetTime) + } + + if feed.Dev >= 0 { + fi.setDevAt(feed.Dev, targetTime) + } + if feed.Ctime >= 0 { + fi.setCtimeAt(feed.Ctime, targetTime) + } + if feed.Inode >= 0 { + fi.setInodeAt(feed.Inode, targetTime) + } + if feed.InodeMode >= 0 { + fi.setInodeModeAt(feed.InodeMode, targetTime) + } +} + +func (fi *FileInfo) setPathAt(path string, targetTime time.Time) { + fi.mutableStrings.Set(fileInfoPath, path, targetTime) +} + +func (fi *FileInfo) setDevAt(dev int, targetTime time.Time) { + fi.mutableInts.Set(fileInfoDev, dev, targetTime) +} + +func (fi *FileInfo) setCtimeAt(ctime int, targetTime time.Time) { + fi.mutableInts.Set(fileInfoCtime, ctime, targetTime) +} + +func (fi *FileInfo) setInodeAt(inode int, targetTime time.Time) { + fi.mutableInts.Set(fileInfoInode, inode, targetTime) +} + +func (fi *FileInfo) setInodeModeAt(inodeMode int, targetTime time.Time) { + fi.mutableInts.Set(fileInfoInodeMode, inodeMode, targetTime) } +// // Getters +// + +// Multiple values at once (getting a feed structure) + +// GetFeed returns the values of the file as a feed. +func (fi *FileInfo) GetFeed() FileInfoFeed { + fi.mutex.RLock() + defer fi.mutex.RUnlock() + + return fi.getFeed() +} + +// GetFeedAt returns the values of the file as a feed at the given time. +func (fi *FileInfo) GetFeedAt(targetTime time.Time) FileInfoFeed { + fi.mutex.RLock() + defer fi.mutex.RUnlock() + + return fi.getFeedAt(targetTime) +} + +// Single values // GetPath returns the path of the file. func (fi *FileInfo) GetPath() string { fi.mutex.RLock() defer fi.mutex.RUnlock() - return fi.path.Get(time.Now()) + + return fi.getPath() } // GetPathAt returns the path of the file at the given time. func (fi *FileInfo) GetPathAt(targetTime time.Time) string { fi.mutex.RLock() defer fi.mutex.RUnlock() - return fi.path.Get(targetTime) + + return fi.getPathAt(targetTime) } // GetDev returns the device number of the file. func (fi *FileInfo) GetDev() int { fi.mutex.RLock() defer fi.mutex.RUnlock() - return fi.dev.Get(time.Now()) + + return fi.getDev() } // GetDevAt returns the device number of the file at the given time. func (fi *FileInfo) GetDevAt(targetTime time.Time) int { fi.mutex.RLock() defer fi.mutex.RUnlock() - return fi.dev.Get(targetTime) + + return fi.getDevAt(targetTime) } // GetCtime returns the creation time of the file. func (fi *FileInfo) GetCtime() int { fi.mutex.RLock() defer fi.mutex.RUnlock() - return fi.ctime.Get(time.Now()) + + return fi.getCtime() } // GetCtimeAt returns the creation time of the file at the given time. func (fi *FileInfo) GetCtimeAt(targetTime time.Time) int { fi.mutex.RLock() defer fi.mutex.RUnlock() - return fi.ctime.Get(targetTime) + + return fi.getCtimeAt(targetTime) } // GetInode returns the inode number of the file. func (fi *FileInfo) GetInode() int { fi.mutex.RLock() defer fi.mutex.RUnlock() - return fi.inode.Get(time.Now()) + + return fi.getInode() } // GetInodeAt returns the inode number of the file at the given time. func (fi *FileInfo) GetInodeAt(targetTime time.Time) int { fi.mutex.RLock() defer fi.mutex.RUnlock() - return fi.inode.Get(targetTime) + + return fi.getInodeAt(targetTime) } // GetInodeMode returns the inode mode of the file. func (fi *FileInfo) GetInodeMode() int { fi.mutex.RLock() defer fi.mutex.RUnlock() - return fi.inodeMode.Get(time.Now()) + + return fi.getInodeMode() } // GetInodeModeAt returns the inode mode of the file at the given time. func (fi *FileInfo) GetInodeModeAt(targetTime time.Time) int { fi.mutex.RLock() defer fi.mutex.RUnlock() - return fi.inodeMode.Get(targetTime) + + return fi.getInodeModeAt(targetTime) +} + +// private getters + +func (fi *FileInfo) getFeed() FileInfoFeed { + return FileInfoFeed{ + Path: fi.getPath(), + Dev: fi.getDev(), + Ctime: fi.getCtime(), + Inode: fi.getInode(), + InodeMode: fi.getInodeMode(), + } +} + +func (fi *FileInfo) getFeedAt(targetTime time.Time) FileInfoFeed { + return FileInfoFeed{ + Path: fi.getPathAt(targetTime), + Dev: fi.getDevAt(targetTime), + Ctime: fi.getCtimeAt(targetTime), + Inode: fi.getInodeAt(targetTime), + InodeMode: fi.getInodeModeAt(targetTime), + } +} + +func (fi *FileInfo) getPath() string { + return fi.mutableStrings.GetCurrent(fileInfoPath) +} + +func (fi *FileInfo) getPathAt(targetTime time.Time) string { + return fi.mutableStrings.Get(fileInfoPath, targetTime) +} + +func (fi *FileInfo) getDev() int { + return fi.mutableInts.GetCurrent(fileInfoDev) +} + +func (fi *FileInfo) getDevAt(targetTime time.Time) int { + return fi.mutableInts.Get(fileInfoDev, targetTime) +} + +func (fi *FileInfo) getCtime() int { + return fi.mutableInts.GetCurrent(fileInfoCtime) +} + +func (fi *FileInfo) getCtimeAt(targetTime time.Time) int { + return fi.mutableInts.Get(fileInfoCtime, targetTime) +} + +func (fi *FileInfo) getInode() int { + return fi.mutableInts.GetCurrent(fileInfoInode) +} + +func (fi *FileInfo) getInodeAt(targetTime time.Time) int { + return fi.mutableInts.Get(fileInfoInode, targetTime) +} + +func (fi *FileInfo) getInodeMode() int { + return fi.mutableInts.GetCurrent(fileInfoInodeMode) +} + +func (fi *FileInfo) getInodeModeAt(targetTime time.Time) int { + return fi.mutableInts.Get(fileInfoInodeMode, targetTime) } diff --git a/pkg/proctree/process.go b/pkg/proctree/process.go index 172176810090..7f6c54a24161 100644 --- a/pkg/proctree/process.go +++ b/pkg/proctree/process.go @@ -22,22 +22,15 @@ type Process struct { mutex *sync.RWMutex // mutex to protect the process } -const ( - executableChangelogSize = 5 // Binary's history is much more important to save - // TODO: Decide whether remove the interpreter and interp from the tree or add them back - interpreterChangelogSize = 0 - interpChangelogSize = 0 -) - // NewProcess creates a new process. func NewProcess(hash uint32) *Process { return &Process{ processHash: hash, parentHash: 0, info: NewTaskInfo(), - executable: NewFileInfo(executableChangelogSize), - interpreter: NewFileInfo(interpreterChangelogSize), - interp: NewFileInfo(interpChangelogSize), + executable: NewFileInfo(), + interpreter: NewFileInfo(), + interp: NewFileInfo(), children: make(map[uint32]struct{}), threads: make(map[uint32]struct{}), mutex: &sync.RWMutex{}, @@ -50,9 +43,9 @@ func NewProcessWithInfo(hash uint32, info *TaskInfo) *Process { processHash: hash, parentHash: 0, info: info, - executable: NewFileInfo(executableChangelogSize), - interpreter: NewFileInfo(interpreterChangelogSize), - interp: NewFileInfo(interpChangelogSize), + executable: NewFileInfo(), + interpreter: NewFileInfo(), + interp: NewFileInfo(), children: make(map[uint32]struct{}), threads: make(map[uint32]struct{}), mutex: &sync.RWMutex{}, diff --git a/pkg/proctree/taskinfo.go b/pkg/proctree/taskinfo.go index 15c5864238bf..45b91ff26477 100644 --- a/pkg/proctree/taskinfo.go +++ b/pkg/proctree/taskinfo.go @@ -4,7 +4,7 @@ import ( "sync" "time" - ch "github.com/aquasecurity/tracee/pkg/changelog" + "github.com/aquasecurity/tracee/pkg/changelog" traceetime "github.com/aquasecurity/tracee/pkg/time" ) @@ -27,145 +27,108 @@ type TaskInfoFeed struct { // Task Info // +const ( + // string members + taskInfoName changelog.MemberKind = iota +) + +const ( + // int members + taskInfoPPid changelog.MemberKind = iota + taskInfoNsPPid + taskInfoUid + taskInfoGid +) + +var ( + // taskInfoMutableStringsFlags is a slice with metadata about the mutable string members of a TaskInfo. + taskInfoMutableStringsFlags = []changelog.MaxEntries{ + taskInfoName: 3, // process name can be changed + } + + // taskInfoMutableIntsFlags is a slice with metadata about the mutable int members of a TaskInfo. + taskInfoMutableIntsFlags = []changelog.MaxEntries{ + taskInfoPPid: 2, // process can be reparented + taskInfoNsPPid: 2, // process can be reparented + taskInfoUid: 2, // process uid can be changed + taskInfoGid: 2, // process gid can be changed + } +) + // TaskInfo represents a task. type TaskInfo struct { - name *ch.Changelog[string] // variable (process name can be changed) - tid int // immutable - pid int // immutable - pPid *ch.Changelog[int] // variable (process can be reparented) - nsTid int // immutable - nsPid int // immutable - nsPPid *ch.Changelog[int] // variable (process can be reparented) - uid *ch.Changelog[int] // variable (process uid can be changed) - gid *ch.Changelog[int] // variable (process gid can be changed) - startTimeNS uint64 // this is a duration, in ns, since boot (immutable) - exitTimeNS uint64 // this is a duration, in ns, since boot (immutable) - mutex *sync.RWMutex + tid int // immutable + pid int // immutable + nsTid int // immutable + nsPid int // immutable + startTimeNS uint64 // this is a duration, in ns, since boot (immutable) + exitTimeNS uint64 // this is a duration, in ns, since boot (immutable) + mutableStrings *changelog.Entries[string] // string mutable fields + mutableInts *changelog.Entries[int] // int mutable fields + mutex *sync.RWMutex } // NewTaskInfo creates a new task. func NewTaskInfo() *TaskInfo { return &TaskInfo{ - name: ch.NewChangelog[string](5), - // All the folloowing values changes are currently not monitored by the process tree. - // Hence, for now, they will only contain one value in the changelog - pPid: ch.NewChangelog[int](1), - nsPPid: ch.NewChangelog[int](1), - uid: ch.NewChangelog[int](1), - gid: ch.NewChangelog[int](1), - mutex: &sync.RWMutex{}, + mutableStrings: changelog.NewEntries[string](taskInfoMutableStringsFlags), + mutableInts: changelog.NewEntries[int](taskInfoMutableIntsFlags), + mutex: &sync.RWMutex{}, } } // NewTaskInfoFromFeed creates a new task with values from the given feed. -func NewTaskInfoFromFeed(feed TaskInfoFeed) *TaskInfo { +func NewTaskInfoNewFromFeed(feed TaskInfoFeed) *TaskInfo { new := NewTaskInfo() - new.SetFeed(feed) + new.setFeed(feed) return new } -// Feed: Multiple values at once. +// +// Setters +// + +// Multiple values at once (using a feed structure) -// SetFeed sets the values of the task from the given feed. +// SetFeed sets the values of the task from the given feed at the current time. func (ti *TaskInfo) SetFeed(feed TaskInfoFeed) { ti.mutex.Lock() defer ti.mutex.Unlock() - ti.setFeedAt(feed, time.Now()) // set current values + + ti.setFeed(feed) } // SetFeedAt sets the values of the task from the given feed at the given time. func (ti *TaskInfo) SetFeedAt(feed TaskInfoFeed, targetTime time.Time) { ti.mutex.Lock() defer ti.mutex.Unlock() - ti.setFeedAt(feed, targetTime) // set values at the given time -} - -func (ti *TaskInfo) setFeedAt(feed TaskInfoFeed, targetTime time.Time) { - if feed.Name != "" { - ti.name.Set(feed.Name, targetTime) - } - if feed.Tid >= 0 { - ti.tid = feed.Tid - } - if feed.Pid >= 0 { - ti.pid = feed.Pid - } - if feed.PPid >= 0 { - ti.pPid.Set(feed.PPid, targetTime) - } - if feed.NsTid >= 0 { - ti.nsTid = feed.NsTid - } - if feed.NsPid >= 0 { - ti.nsPid = feed.NsPid - } - if feed.NsPid >= 0 { - ti.nsPPid.Set(feed.NsPid, targetTime) - } - if feed.Uid >= 0 { - ti.uid.Set(feed.Uid, targetTime) - } - if feed.Gid >= 0 { - ti.gid.Set(feed.Gid, targetTime) - } - if feed.StartTimeNS != 0 { - ti.startTimeNS = feed.StartTimeNS - } - if feed.ExitTimeNS != 0 { - ti.exitTimeNS = feed.ExitTimeNS - } -} - -// GetFeed returns the values of the task as a feed. -func (ti *TaskInfo) GetFeed() TaskInfoFeed { - ti.mutex.RLock() - defer ti.mutex.RUnlock() - return ti.getFeedAt(time.Now()) // return current values -} -// GetFeedAt returns the values of the task as a feed at the given time. -func (ti *TaskInfo) GetFeedAt(targetTime time.Time) TaskInfoFeed { - ti.mutex.RLock() - defer ti.mutex.RUnlock() - return ti.getFeedAt(targetTime) // return values at the given time -} - -func (ti *TaskInfo) getFeedAt(targetTime time.Time) TaskInfoFeed { - return TaskInfoFeed{ - Name: ti.name.Get(targetTime), - Tid: ti.tid, - Pid: ti.pid, - PPid: ti.pPid.Get(targetTime), - NsTid: ti.nsTid, - NsPid: ti.nsPid, - NsPPid: ti.nsPPid.Get(targetTime), - Uid: ti.uid.Get(targetTime), - Gid: ti.gid.Get(targetTime), - StartTimeNS: ti.startTimeNS, - ExitTimeNS: ti.exitTimeNS, - } + ti.setFeedAt(feed, targetTime) } -// Setters +// Single values // SetName sets the name of the task. func (ti *TaskInfo) SetName(name string) { ti.mutex.Lock() defer ti.mutex.Unlock() - ti.name.Set(name, time.Now()) + + ti.setNameAt(name, time.Now()) } // SetNameAt sets the name of the task at the given time. func (ti *TaskInfo) SetNameAt(name string, targetTime time.Time) { ti.mutex.Lock() defer ti.mutex.Unlock() - ti.name.Set(name, targetTime) + + ti.setNameAt(name, targetTime) } // SetTid sets the tid of the task. func (ti *TaskInfo) SetTid(tid int) { ti.mutex.Lock() defer ti.mutex.Unlock() + ti.tid = tid } @@ -173,6 +136,7 @@ func (ti *TaskInfo) SetTid(tid int) { func (ti *TaskInfo) SetPid(pid int) { ti.mutex.Lock() defer ti.mutex.Unlock() + ti.pid = pid } @@ -180,6 +144,7 @@ func (ti *TaskInfo) SetPid(pid int) { func (ti *TaskInfo) SetNsTid(nsTid int) { ti.mutex.Lock() defer ti.mutex.Unlock() + ti.nsTid = nsTid } @@ -187,6 +152,7 @@ func (ti *TaskInfo) SetNsTid(nsTid int) { func (ti *TaskInfo) SetNsPid(nsPid int) { ti.mutex.Lock() defer ti.mutex.Unlock() + ti.nsPid = nsPid } @@ -194,6 +160,7 @@ func (ti *TaskInfo) SetNsPid(nsPid int) { func (ti *TaskInfo) SetStartTimeNS(startTimeNS uint64) { ti.mutex.Lock() defer ti.mutex.Unlock() + ti.startTimeNS = startTimeNS } @@ -201,6 +168,7 @@ func (ti *TaskInfo) SetStartTimeNS(startTimeNS uint64) { func (ti *TaskInfo) SetExitTime(exitTime uint64) { ti.mutex.Lock() defer ti.mutex.Unlock() + ti.exitTimeNS = exitTime } @@ -208,78 +176,173 @@ func (ti *TaskInfo) SetExitTime(exitTime uint64) { func (ti *TaskInfo) SetPPid(pPid int) { ti.mutex.Lock() defer ti.mutex.Unlock() - ti.pPid.Set(pPid, time.Now()) + + ti.setPPidAt(pPid, time.Now()) } // SetPPidAt sets the ppid of the task at the given time. func (ti *TaskInfo) SetPPidAt(pPid int, targetTime time.Time) { ti.mutex.Lock() defer ti.mutex.Unlock() - ti.pPid.Set(pPid, targetTime) + + ti.setPPidAt(pPid, targetTime) } // SetNsPPid sets the nsppid of the task. func (ti *TaskInfo) SetNsPPid(nsPPid int) { ti.mutex.Lock() defer ti.mutex.Unlock() - ti.nsPPid.Set(nsPPid, time.Now()) + + ti.setNsPPidAt(nsPPid, time.Now()) } // SetNsPPidAt sets the nsppid of the task at the given time. func (ti *TaskInfo) SetNsPPidAt(nsPPid int, targetTime time.Time) { ti.mutex.Lock() defer ti.mutex.Unlock() - ti.nsPPid.Set(nsPPid, targetTime) + + ti.setNsPPidAt(nsPPid, targetTime) } // SetUid sets the uid of the task. func (ti *TaskInfo) SetUid(uid int) { ti.mutex.Lock() defer ti.mutex.Unlock() - ti.uid.Set(uid, time.Now()) + + ti.setUidAt(uid, time.Now()) } // SetUidAt sets the uid of the task at the given time. func (ti *TaskInfo) SetUidAt(uid int, targetTime time.Time) { ti.mutex.Lock() defer ti.mutex.Unlock() - ti.uid.Set(uid, targetTime) + + ti.setUidAt(uid, targetTime) } // SetGid sets the gid of the task. func (ti *TaskInfo) SetGid(gid int) { ti.mutex.Lock() defer ti.mutex.Unlock() - ti.gid.Set(gid, time.Now()) + + ti.setGidAt(gid, time.Now()) } // SetGidAt sets the gid of the task at the given time. func (ti *TaskInfo) SetGidAt(gid int, targetTime time.Time) { ti.mutex.Lock() defer ti.mutex.Unlock() - ti.gid.Set(gid, targetTime) + + ti.setGidAt(gid, targetTime) +} + +// private setters + +func (ti *TaskInfo) setFeed(feed TaskInfoFeed) { + ti.setFeedAt(feed, time.Now()) +} + +func (ti *TaskInfo) setFeedAt(feed TaskInfoFeed, targetTime time.Time) { + if feed.Name != "" { + ti.setNameAt(feed.Name, targetTime) + } + if feed.Tid >= 0 { + ti.tid = feed.Tid + } + if feed.Pid >= 0 { + ti.pid = feed.Pid + } + if feed.PPid >= 0 { + ti.setPPidAt(feed.PPid, targetTime) + } + if feed.NsTid >= 0 { + ti.nsTid = feed.NsTid + } + if feed.NsPid >= 0 { + ti.nsPid = feed.NsPid + } + if feed.NsPid >= 0 { + ti.setNsPPidAt(feed.NsPPid, targetTime) + } + if feed.Uid >= 0 { + ti.setUidAt(feed.Uid, targetTime) + } + if feed.Gid >= 0 { + ti.setGidAt(feed.Gid, targetTime) + } + if feed.StartTimeNS != 0 { + ti.startTimeNS = feed.StartTimeNS + } + if feed.ExitTimeNS != 0 { + ti.exitTimeNS = feed.ExitTimeNS + } +} + +func (ti *TaskInfo) setNameAt(name string, targetTime time.Time) { + ti.mutableStrings.Set(taskInfoName, name, targetTime) +} + +func (ti *TaskInfo) setPPidAt(pPid int, targetTime time.Time) { + ti.mutableInts.Set(taskInfoPPid, pPid, targetTime) +} + +func (ti *TaskInfo) setNsPPidAt(nsPPid int, targetTime time.Time) { + ti.mutableInts.Set(taskInfoNsPPid, nsPPid, targetTime) } +func (ti *TaskInfo) setUidAt(uid int, targetTime time.Time) { + ti.mutableInts.Set(taskInfoUid, uid, targetTime) +} + +func (ti *TaskInfo) setGidAt(gid int, targetTime time.Time) { + ti.mutableInts.Set(taskInfoGid, gid, targetTime) +} + +// // Getters +// + +// Multiple values at once (getting a feed structure) + +// GetFeed returns the values of the task as a feed. +func (ti *TaskInfo) GetFeed() TaskInfoFeed { + ti.mutex.RLock() + defer ti.mutex.RUnlock() + + return ti.getFeed() +} + +// GetFeedAt returns the values of the task as a feed at the given time. +func (ti *TaskInfo) GetFeedAt(targetTime time.Time) TaskInfoFeed { + ti.mutex.RLock() + defer ti.mutex.RUnlock() + + return ti.getFeedAt(targetTime) +} + +// Single values // GetName returns the name of the task. func (ti *TaskInfo) GetName() string { ti.mutex.RLock() defer ti.mutex.RUnlock() - return ti.name.GetCurrent() + + return ti.getName() } // GetNameAt returns the name of the task at the given time. func (ti *TaskInfo) GetNameAt(targetTime time.Time) string { ti.mutex.RLock() defer ti.mutex.RUnlock() - return ti.name.Get(targetTime) + + return ti.getNameAt(targetTime) } // GetTid returns the tid of the task. func (ti *TaskInfo) GetTid() int { ti.mutex.RLock() defer ti.mutex.RUnlock() + return ti.tid } @@ -287,6 +350,7 @@ func (ti *TaskInfo) GetTid() int { func (ti *TaskInfo) GetPid() int { ti.mutex.RLock() defer ti.mutex.RUnlock() + return ti.pid } @@ -294,6 +358,7 @@ func (ti *TaskInfo) GetPid() int { func (ti *TaskInfo) GetNsTid() int { ti.mutex.RLock() defer ti.mutex.RUnlock() + return ti.nsTid } @@ -301,6 +366,7 @@ func (ti *TaskInfo) GetNsTid() int { func (ti *TaskInfo) GetNsPid() int { ti.mutex.RLock() defer ti.mutex.RUnlock() + return ti.nsPid } @@ -308,62 +374,71 @@ func (ti *TaskInfo) GetNsPid() int { func (ti *TaskInfo) GetPPid() int { ti.mutex.RLock() defer ti.mutex.RUnlock() - return ti.pPid.GetCurrent() + + return ti.getPPid() } // GetPPidAt returns the ppid of the task at the given time. func (ti *TaskInfo) GetPPidAt(targetTime time.Time) int { ti.mutex.RLock() defer ti.mutex.RUnlock() - return ti.pPid.Get(targetTime) + + return ti.getPPidAt(targetTime) } // GetNsPPid returns the nsPPid of the task. func (ti *TaskInfo) GetNsPPid() int { ti.mutex.RLock() defer ti.mutex.RUnlock() - return ti.nsPPid.GetCurrent() + + return ti.getNsPPid() } // GetNsPPidAt returns the nsPPid of the task at the given time. func (ti *TaskInfo) GetNsPPidAt(targetTime time.Time) int { ti.mutex.RLock() defer ti.mutex.RUnlock() - return ti.nsPPid.Get(targetTime) + + return ti.getNsPPidAt(targetTime) } // GetUid returns the uid of the task. func (ti *TaskInfo) GetUid() int { ti.mutex.RLock() defer ti.mutex.RUnlock() - return ti.uid.GetCurrent() + + return ti.getUid() } // GetUidAt returns the uid of the task at the given time. func (ti *TaskInfo) GetUidAt(targetTime time.Time) int { ti.mutex.RLock() defer ti.mutex.RUnlock() - return ti.uid.Get(targetTime) + + return ti.getUidAt(targetTime) } // GetGid returns the gid of the task. func (ti *TaskInfo) GetGid() int { ti.mutex.RLock() defer ti.mutex.RUnlock() - return ti.gid.GetCurrent() + + return ti.getGid() } // GetGidAt returns the gid of the task at the given time. func (ti *TaskInfo) GetGidAt(targetTime time.Time) int { ti.mutex.RLock() defer ti.mutex.RUnlock() - return ti.gid.Get(targetTime) + + return ti.getGidAt(targetTime) } // GetStartTimeNS returns the start time of the task in nanoseconds since epoch func (ti *TaskInfo) GetStartTimeNS() uint64 { ti.mutex.RLock() defer ti.mutex.RUnlock() + return ti.startTimeNS } @@ -379,6 +454,7 @@ func (ti *TaskInfo) GetStartTime() time.Time { func (ti *TaskInfo) GetExitTimeNS() uint64 { ti.mutex.RLock() defer ti.mutex.RUnlock() + return ti.exitTimeNS } @@ -386,6 +462,7 @@ func (ti *TaskInfo) GetExitTimeNS() uint64 { func (ti *TaskInfo) GetExitTime() time.Time { ti.mutex.RLock() defer ti.mutex.RUnlock() + return traceetime.NsSinceEpochToTime(ti.exitTimeNS) } @@ -393,6 +470,7 @@ func (ti *TaskInfo) GetExitTime() time.Time { func (ti *TaskInfo) IsAlive() bool { ti.mutex.RLock() defer ti.mutex.RUnlock() + return ti.exitTimeNS == 0 } @@ -401,6 +479,7 @@ func (ti *TaskInfo) IsAlive() bool { func (ti *TaskInfo) IsAliveAt(targetTime time.Time) bool { ti.mutex.RLock() defer ti.mutex.RUnlock() + if ti.exitTimeNS != 0 { if targetTime.After(traceetime.NsSinceEpochToTime(ti.exitTimeNS)) { return false @@ -411,5 +490,80 @@ func (ti *TaskInfo) IsAliveAt(targetTime time.Time) bool { if targetTime.Before(traceetime.NsSinceEpochToTime(ti.startTimeNS)) { return false } + return true } + +// private getters + +func (ti *TaskInfo) getFeed() TaskInfoFeed { + return TaskInfoFeed{ + Name: ti.getName(), + Tid: ti.tid, + Pid: ti.pid, + PPid: ti.getPPid(), + NsTid: ti.nsTid, + NsPid: ti.nsPid, + NsPPid: ti.getNsPPid(), + Uid: ti.getUid(), + Gid: ti.getGid(), + StartTimeNS: ti.startTimeNS, + ExitTimeNS: ti.exitTimeNS, + } +} + +func (ti *TaskInfo) getFeedAt(targetTime time.Time) TaskInfoFeed { + return TaskInfoFeed{ + Name: ti.getNameAt(targetTime), + Tid: ti.tid, + Pid: ti.pid, + PPid: ti.getPPidAt(targetTime), + NsTid: ti.nsTid, + NsPid: ti.nsPid, + NsPPid: ti.getNsPPidAt(targetTime), + Uid: ti.getUidAt(targetTime), + Gid: ti.getGidAt(targetTime), + StartTimeNS: ti.startTimeNS, + ExitTimeNS: ti.exitTimeNS, + } +} + +func (ti *TaskInfo) getName() string { + return ti.mutableStrings.GetCurrent(taskInfoName) +} + +func (ti *TaskInfo) getNameAt(targetTime time.Time) string { + return ti.mutableStrings.Get(taskInfoName, targetTime) +} + +func (ti *TaskInfo) getPPid() int { + return ti.mutableInts.GetCurrent(taskInfoPPid) +} + +func (ti *TaskInfo) getPPidAt(targetTime time.Time) int { + return ti.mutableInts.Get(taskInfoPPid, targetTime) +} + +func (ti *TaskInfo) getNsPPid() int { + return ti.mutableInts.GetCurrent(taskInfoNsPPid) +} + +func (ti *TaskInfo) getNsPPidAt(targetTime time.Time) int { + return ti.mutableInts.Get(taskInfoNsPPid, targetTime) +} + +func (ti *TaskInfo) getUid() int { + return ti.mutableInts.GetCurrent(taskInfoUid) +} + +func (ti *TaskInfo) getUidAt(targetTime time.Time) int { + return ti.mutableInts.Get(taskInfoUid, targetTime) +} + +func (ti *TaskInfo) getGid() int { + return ti.mutableInts.GetCurrent(taskInfoGid) +} + +func (ti *TaskInfo) getGidAt(targetTime time.Time) int { + return ti.mutableInts.Get(taskInfoGid, targetTime) +}