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))
}