diff --git a/README.md b/README.md index 95ab6587..820049a0 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,7 @@ INPUT: -lip, -listen-ip string public ip address to listen on (default "0.0.0.0") -e, -eviction int number of days to persist interaction data in memory (default 30) -ne, -no-eviction disable periodic data eviction from memory + -es, -eviction-strategy string eviction strategy for interactions (sliding, fixed) (default "sliding") -a, -auth enable authentication to server using random generated token -t, -token string enable authentication to server using given token -acao-url string origin url to send in acao header to use web-client) (default "*") diff --git a/cmd/interactsh-server/main.go b/cmd/interactsh-server/main.go index 49825781..f22ec846 100644 --- a/cmd/interactsh-server/main.go +++ b/cmd/interactsh-server/main.go @@ -49,6 +49,7 @@ func main() { flagSet.StringVarP(&cliOptions.ListenIP, "listen-ip", "lip", "0.0.0.0", "public ip address to listen on"), flagSet.IntVarP(&cliOptions.Eviction, "eviction", "e", 30, "number of days to persist interaction data in memory"), flagSet.BoolVarP(&cliOptions.NoEviction, "no-eviction", "ne", false, "disable periodic data eviction from memory"), + flagSet.StringVarP(&cliOptions.EvictionStrategy, "eviction-strategy", "es", "sliding", "eviction strategy for interactions (sliding, fixed)"), flagSet.BoolVarP(&cliOptions.Auth, "auth", "a", false, "enable authentication to server using random generated token"), flagSet.StringVarP(&cliOptions.Token, "token", "t", "", "enable authentication to server using given token"), flagSet.StringVar(&cliOptions.OriginURL, "acao-url", "*", "origin url to send in acao header to use web-client)"), // cli flag set to deprecate @@ -230,9 +231,22 @@ func main() { if cliOptions.NoEviction { evictionTTL = -1 } + + // Parse eviction strategy + var evictionStrategy storage.EvictionStrategy + switch strings.ToLower(cliOptions.EvictionStrategy) { + case "fixed": + evictionStrategy = storage.EvictionStrategyFixed + case "sliding": + evictionStrategy = storage.EvictionStrategySliding + default: + gologger.Fatal().Msgf("invalid eviction strategy '%s', must be 'sliding' or 'fixed'\n", cliOptions.EvictionStrategy) + } + var store storage.Storage storeOptions := storage.DefaultOptions storeOptions.EvictionTTL = evictionTTL + storeOptions.EvictionStrategy = evictionStrategy if cliOptions.DiskStorage { if cliOptions.DiskStoragePath == "" { gologger.Fatal().Msgf("disk storage path must be specified\n") diff --git a/pkg/options/server_options.go b/pkg/options/server_options.go index 460e579f..cbf08862 100644 --- a/pkg/options/server_options.go +++ b/pkg/options/server_options.go @@ -23,6 +23,7 @@ type CLIServerOptions struct { LdapWithFullLogger bool Eviction int NoEviction bool + EvictionStrategy string Responder bool Smb bool SmbPort int diff --git a/pkg/storage/option.go b/pkg/storage/option.go index ce827b8b..04bd0f18 100644 --- a/pkg/storage/option.go +++ b/pkg/storage/option.go @@ -2,10 +2,18 @@ package storage import "time" +type EvictionStrategy int + +const ( + EvictionStrategySliding EvictionStrategy = iota // expire-after-access + EvictionStrategyFixed // expire-after-write +) + type Options struct { - DbPath string - EvictionTTL time.Duration - MaxSize int + DbPath string + EvictionTTL time.Duration + MaxSize int + EvictionStrategy EvictionStrategy } func (options *Options) UseDisk() bool { @@ -13,5 +21,6 @@ func (options *Options) UseDisk() bool { } var DefaultOptions = Options{ - MaxSize: 2500000, + MaxSize: 2500000, + EvictionStrategy: EvictionStrategySliding, } diff --git a/pkg/storage/storagedb.go b/pkg/storage/storagedb.go index 4170f25c..524f33e8 100644 --- a/pkg/storage/storagedb.go +++ b/pkg/storage/storagedb.go @@ -37,7 +37,14 @@ func New(options *Options) (*StorageDB, error) { cache.WithMaximumSize(options.MaxSize), } if options.EvictionTTL > 0 { - cacheOptions = append(cacheOptions, cache.WithExpireAfterAccess(options.EvictionTTL)) + switch options.EvictionStrategy { + case EvictionStrategyFixed: + cacheOptions = append(cacheOptions, cache.WithExpireAfterWrite(options.EvictionTTL)) + case EvictionStrategySliding: + fallthrough + default: + cacheOptions = append(cacheOptions, cache.WithExpireAfterAccess(options.EvictionTTL)) + } } if options.UseDisk() { cacheOptions = append(cacheOptions, cache.WithRemovalListener(storageDB.OnCacheRemovalCallback)) diff --git a/pkg/storage/storagedb_test.go b/pkg/storage/storagedb_test.go index fcd27509..45e564ed 100644 --- a/pkg/storage/storagedb_test.go +++ b/pkg/storage/storagedb_test.go @@ -133,3 +133,49 @@ func doStuffWithOtherCache(cache cache.Cache) { _, _ = cache.GetIfPresent(strconv.Itoa(i)) } } + +func TestSlidingEvictionStrategy(t *testing.T) { + testTTL := 100 * time.Millisecond + smallDelay := 10 * time.Millisecond + mem, err := New(&Options{EvictionTTL: testTTL, EvictionStrategy: EvictionStrategySliding}) + require.Nil(t, err) + defer mem.Close() + + err = mem.SetID("test-sliding") + require.Nil(t, err) + + // Access after half TTL - should extend expiration + time.Sleep(testTTL / 2) + _, ok := mem.cache.GetIfPresent("test-sliding") + require.True(t, ok) + + // Still present after original TTL due to sliding window + time.Sleep(testTTL / 2 + smallDelay) + _, ok = mem.cache.GetIfPresent("test-sliding") + require.True(t, ok) + + // Should be expired after full TTL despite access + time.Sleep(testTTL + smallDelay) + _, ok = mem.cache.GetIfPresent("test-sliding") + require.False(t, ok) +} + +func TestFixedEvictionStrategy(t *testing.T) { + testTTL := 100 * time.Millisecond + mem, err := New(&Options{EvictionTTL: testTTL, EvictionStrategy: EvictionStrategyFixed}) + require.Nil(t, err) + defer mem.Close() + + err = mem.SetID("test-fixed") + require.Nil(t, err) + + // Access after half TTL - should NOT extend expiration + time.Sleep(testTTL / 2) + _, ok := mem.cache.GetIfPresent("test-fixed") + require.True(t, ok) + + // Should be expired after full TTL despite access + time.Sleep(testTTL / 2 + 10 * time.Millisecond) + _, ok = mem.cache.GetIfPresent("test-fixed") + require.False(t, ok) +}