diff --git a/html/index.html b/html/index.html index 36f09d4b..fe836476 100644 --- a/html/index.html +++ b/html/index.html @@ -2131,7 +2131,7 @@

{{ i18n.features }}: - + {{ $root.localizeMessage(feature) }} diff --git a/licensing/license_manager.go b/licensing/license_manager.go index 6e7ba259..0c37ca1c 100644 --- a/licensing/license_manager.go +++ b/licensing/license_manager.go @@ -46,6 +46,7 @@ const FEAT_NTF = "ntf" const FEAT_ODC = "odc" const FEAT_STG = "stg" const FEAT_TTR = "ttr" +const FEAT_RPT = "rpt" const PUBLIC_KEY = ` -----BEGIN PUBLIC KEY----- @@ -173,6 +174,7 @@ func CreateAvailableFeatureList() []string { available = append(available, FEAT_ODC) available = append(available, FEAT_STG) available = append(available, FEAT_TTR) + available = append(available, FEAT_RPT) return available } diff --git a/licensing/license_manager_test.go b/licensing/license_manager_test.go index b4957f02..7eade074 100644 --- a/licensing/license_manager_test.go +++ b/licensing/license_manager_test.go @@ -123,7 +123,7 @@ func TestListAvailableFeatures(tester *testing.T) { Init(EXPIRED_KEY) manager.status = LICENSE_STATUS_ACTIVE - assert.Len(tester, ListAvailableFeatures(), 7) + assert.Len(tester, ListAvailableFeatures(), 8) assert.Equal(tester, ListAvailableFeatures()[0], FEAT_FPS) assert.Equal(tester, ListAvailableFeatures()[1], FEAT_GMD) assert.Equal(tester, ListAvailableFeatures()[2], FEAT_LKS) @@ -131,6 +131,7 @@ func TestListAvailableFeatures(tester *testing.T) { assert.Equal(tester, ListAvailableFeatures()[4], FEAT_ODC) assert.Equal(tester, ListAvailableFeatures()[5], FEAT_STG) assert.Equal(tester, ListAvailableFeatures()[6], FEAT_TTR) + assert.Equal(tester, ListAvailableFeatures()[7], FEAT_RPT) } func TestListEnabledFeaturesUnprovisioned(tester *testing.T) { @@ -139,16 +140,6 @@ func TestListEnabledFeaturesUnprovisioned(tester *testing.T) { Init("") assert.Len(tester, ListEnabledFeatures(), 0) - Init(EXPIRED_KEY) - assert.Len(tester, ListEnabledFeatures(), 7) - assert.Equal(tester, ListEnabledFeatures()[0], FEAT_FPS) - assert.Equal(tester, ListEnabledFeatures()[1], FEAT_GMD) - assert.Equal(tester, ListEnabledFeatures()[2], FEAT_LKS) - assert.Equal(tester, ListEnabledFeatures()[3], FEAT_NTF) - assert.Equal(tester, ListEnabledFeatures()[4], FEAT_ODC) - assert.Equal(tester, ListEnabledFeatures()[5], FEAT_STG) - assert.Equal(tester, ListEnabledFeatures()[6], FEAT_TTR) - Init(EXPIRED_KEY) manager.licenseKey.Features = append(manager.licenseKey.Features, "foo") manager.licenseKey.Features = append(manager.licenseKey.Features, "bar") @@ -166,14 +157,14 @@ func TestGetLicenseKey(tester *testing.T) { assert.Equal(tester, key.Nodes, 1) assert.Equal(tester, key.SocUrl, "https://somewhere.invalid") assert.Equal(tester, key.DataUrl, "https://another.place") - assert.Len(tester, key.Features, 7) + assert.Len(tester, key.Features, 8) // Modify the returned object and make sure it doesn't affect the orig object key.Users = 100 key.Features = append(key.Features, "foo") assert.Equal(tester, GetLicenseKey().Users, 1) - assert.Len(tester, key.Features, 8) - assert.Len(tester, GetLicenseKey().Features, 7) + assert.Len(tester, key.Features, 9) + assert.Len(tester, GetLicenseKey().Features, 8) } func TestGetStatus(tester *testing.T) { diff --git a/server/modules/elastic/elasticeventstore.go b/server/modules/elastic/elasticeventstore.go index 115b8126..882175c9 100644 --- a/server/modules/elastic/elasticeventstore.go +++ b/server/modules/elastic/elasticeventstore.go @@ -21,6 +21,7 @@ import ( "github.com/apex/log" "github.com/elastic/go-elasticsearch/v8" "github.com/elastic/go-elasticsearch/v8/esapi" + "github.com/security-onion-solutions/securityonion-soc/licensing" "github.com/security-onion-solutions/securityonion-soc/model" "github.com/security-onion-solutions/securityonion-soc/server" "github.com/security-onion-solutions/securityonion-soc/web" @@ -954,6 +955,44 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(ctx context.Context, idF return nil } +func (store *ElasticEventstore) addUpdateScripts(updateCriteria *model.EventUpdateCriteria, timeNow time.Time, ack bool, esc bool) { + if ack { + trackTiming := strconv.FormatBool(licensing.IsEnabled(licensing.FEAT_RPT)) + escBool := strconv.FormatBool(esc) + nowMillis := timeNow.UnixMilli() + nowMillisStr := strconv.FormatInt(nowMillis, 10) + updateCriteria.AddUpdateScript(` + boolean track_timing = ` + trackTiming + `; + boolean esc_bool = ` + escBool + `; + Instant now_instant = Instant.ofEpochMilli(` + nowMillisStr + `L); + ZonedDateTime now_date = ZonedDateTime.ofInstant(now_instant, ZoneId.of('Z')); + long elapsed_seconds = 0; + if (ctx._source.containsKey('@timestamp')) { + ZonedDateTime event_date = ZonedDateTime.parse(ctx._source['@timestamp']); + elapsed_seconds = ChronoUnit.SECONDS.between(event_date, now_date) + } + + if (ctx._source.event.acknowledged != true) { + ctx._source.event.acknowledged = true; + if (track_timing) { + ctx._source.event.acknowledged_timestamp = now_date; + ctx._source.event.acknowledged_elapsed_seconds = elapsed_seconds; + } + } + + if (ctx._source.event.escalated != true && esc_bool) { + ctx._source.event.escalated = esc_bool; + if (track_timing) { + ctx._source.event.escalated_timestamp = now_date; + ctx._source.event.escalated_elapsed_seconds = elapsed_seconds; + } + } + `) + } else { + updateCriteria.AddUpdateScript(`ctx._source.event.acknowledged = false;`) + } +} + func (store *ElasticEventstore) Acknowledge(ctx context.Context, ackCriteria *model.EventAckCriteria) (*model.EventUpdateResults, error) { var results *model.EventUpdateResults var err error @@ -968,10 +1007,7 @@ func (store *ElasticEventstore) Acknowledge(ctx context.Context, ackCriteria *mo }).Info("Acknowledging event") updateCriteria := model.NewEventUpdateCriteria() - updateCriteria.AddUpdateScript("ctx._source.event.acknowledged=" + strconv.FormatBool(ackCriteria.Acknowledge)) - if ackCriteria.Escalate && ackCriteria.Acknowledge { - updateCriteria.AddUpdateScript("ctx._source.event.escalated=true") - } + store.addUpdateScripts(updateCriteria, time.Now(), ackCriteria.Acknowledge, ackCriteria.Escalate) updateCriteria.Populate(ackCriteria.SearchFilter, ackCriteria.DateRange, ackCriteria.DateRangeFormat, diff --git a/server/modules/elastic/elasticeventstore_test.go b/server/modules/elastic/elasticeventstore_test.go index 89d06d0f..20a111d5 100644 --- a/server/modules/elastic/elasticeventstore_test.go +++ b/server/modules/elastic/elasticeventstore_test.go @@ -720,3 +720,60 @@ func TestScrollMidScrollError(t *testing.T) { assert.Nil(t, err) assert.Equal(t, `{"scroll_id":"MyScrollID"}`, string(body)) } + +func TestAddUpdateScript(t *testing.T) { + client, _ := modmock.NewMockClient(t) + + store := &ElasticEventstore{ + esClient: client, + cacheTime: time.Now().Add(time.Hour), + fieldDefs: make(map[string]*FieldDefinition), + maxScrollSize: 10000, + maxLogLength: math.MaxInt, + index: "myIndex", + } + + timeNow := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + + criteria := &model.EventUpdateCriteria{} + store.addUpdateScripts(criteria, timeNow, false, false) + assert.Len(t, criteria.UpdateScripts, 1) + assert.Equal(t, "ctx._source.event.acknowledged = false;", criteria.UpdateScripts[0]) + + criteria = &model.EventUpdateCriteria{} + store.addUpdateScripts(criteria, timeNow, true, false) + assert.Len(t, criteria.UpdateScripts, 1) + expected := ` + boolean track_timing = false; + boolean esc_bool = false; + Instant now_instant = Instant.ofEpochMilli(1257894000000L); + ZonedDateTime now_date = ZonedDateTime.ofInstant(now_instant, ZoneId.of('Z')); + long elapsed_seconds = 0; + if (ctx._source.containsKey('@timestamp')) { + ZonedDateTime event_date = ZonedDateTime.parse(ctx._source['@timestamp']); + elapsed_seconds = ChronoUnit.SECONDS.between(event_date, now_date) + } + + if (ctx._source.event.acknowledged != true) { + ctx._source.event.acknowledged = true; + if (track_timing) { + ctx._source.event.acknowledged_timestamp = now_date; + ctx._source.event.acknowledged_elapsed_seconds = elapsed_seconds; + } + } + + if (ctx._source.event.escalated != true && esc_bool) { + ctx._source.event.escalated = esc_bool; + if (track_timing) { + ctx._source.event.escalated_timestamp = now_date; + ctx._source.event.escalated_elapsed_seconds = elapsed_seconds; + } + } + ` + assert.Equal(t, expected, criteria.UpdateScripts[0]) + + criteria = &model.EventUpdateCriteria{} + store.addUpdateScripts(criteria, timeNow, true, true) + assert.Len(t, criteria.UpdateScripts, 1) + assert.Contains(t, criteria.UpdateScripts[0], "esc_bool = true") +} diff --git a/server/modules/salt/saltstore_test.go b/server/modules/salt/saltstore_test.go index 4151662b..8b5ca852 100644 --- a/server/modules/salt/saltstore_test.go +++ b/server/modules/salt/saltstore_test.go @@ -204,7 +204,11 @@ func TestGetSettings(tester *testing.T) { settings, err := salt.GetSettings(ctx(), true) slices.SortFunc(settings, func(a, b *model.Setting) int { - return cmp.Compare(a.Id, b.Id) + r := cmp.Compare(a.Id, b.Id) + if r == 0 { + r = cmp.Compare(a.NodeId, b.NodeId) + } + return r }) assert.NoError(tester, err) @@ -341,17 +345,17 @@ func TestGetSettings(tester *testing.T) { assert.Equal(tester, "", settings[count].NodeId) count++ - assert.Equal(tester, "myapp.zdef", settings[count].Id) - assert.Equal(tester, "strawberry", settings[count].Value) - assert.Equal(tester, "normal_import", settings[count].NodeId) - count++ - assert.Equal(tester, "myapp.zdef", settings[count].Id) assert.Equal(tester, "chocolate", settings[count].Value) assert.Equal(tester, "vanilla", settings[count].Default) assert.Equal(tester, "", settings[count].NodeId) count++ + assert.Equal(tester, "myapp.zdef", settings[count].Id) + assert.Equal(tester, "strawberry", settings[count].Value) + assert.Equal(tester, "normal_import", settings[count].NodeId) + count++ + assert.Equal(tester, count, len(settings)) }