From f8178db98c54695e1bb435dac6f6986948ee7e73 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Mon, 24 May 2021 11:19:14 -0400 Subject: [PATCH 1/7] Remove searchUsername from Kratos traits; Upgraded Kratos to 0.6.3-alpha.1 --- Dockerfile.kratos | 3 +-- html/index.html | 2 ++ html/js/routes/login.js | 13 +++++++++---- html/js/routes/settings.js | 18 ++++++++++-------- html/login/index.html | 3 ++- server/modules/kratos/kratosuser.go | 3 --- server/modules/kratos/kratosuser_test.go | 7 ------- 7 files changed, 24 insertions(+), 25 deletions(-) diff --git a/Dockerfile.kratos b/Dockerfile.kratos index 30b3cf250..f3294c949 100644 --- a/Dockerfile.kratos +++ b/Dockerfile.kratos @@ -20,13 +20,12 @@ RUN git clone https://github.com/ory/kratos.git WORKDIR /go/src/github.com/ory/kratos -RUN git checkout v0.5.5-alpha.1 +RUN git checkout v0.6.3-alpha.1 ENV GO111MODULE on ENV CGO_ENABLED 1 RUN go mod download -RUN make pack RUN go build -tags sqlite -a FROM ghcr.io/security-onion-solutions/alpine:latest diff --git a/html/index.html b/html/index.html index dfc7e3144..dc0a7f046 100644 --- a/html/index.html +++ b/html/index.html @@ -778,6 +778,7 @@

+ @@ -1169,6 +1170,7 @@

+ diff --git a/html/js/routes/login.js b/html/js/routes/login.js index 44e28279c..080a54aea 100644 --- a/html/js/routes/login.js +++ b/html/js/routes/login.js @@ -18,6 +18,7 @@ routes.push({ path: '*', name: 'login', component: { email: null, password: null, csrfToken: null, + method: null, }, rules: { required: value => !!value || this.$root.i18n.required, @@ -30,7 +31,7 @@ routes.push({ path: '*', name: 'login', component: { this.$root.showLogin(); } else { this.showLoginForm = true; - this.authLoginUrl = this.$root.authUrl + 'login/methods/password' + location.search; + this.authLoginUrl = this.$root.authUrl + 'login' + location.search; this.loadData() } }, @@ -44,9 +45,13 @@ routes.push({ path: '*', name: 'login', component: { this.banner = marked(response.data); } response = await this.$root.authApi.get('login/flows?id=' + this.$root.getAuthFlowId()); - this.form.csrfToken = response.data.methods.password.config.fields.find(item => item.name == 'csrf_token').value; - if (response.data.methods.password.config.messages) { - this.$root.showWarning(this.i18n.loginInvalid); + this.form.csrfToken = response.data.ui.nodes.find(item => item.attributes && item.attributes.name == 'csrf_token').attributes.value; + this.form.method = response.data.ui.nodes.find(item => item.attributes && item.attributes.name == 'method').attributes.value; + if (response.data.ui.messages) { + const error = response.data.ui.messages.find(item => item.type == "error"); + if (error && error.text) { + this.$root.showWarning(this.i18n.loginInvalid); + } } } catch (error) { if (error.response.status == 410) { diff --git a/html/js/routes/settings.js b/html/js/routes/settings.js index 21db2943b..957c603e3 100644 --- a/html/js/routes/settings.js +++ b/html/js/routes/settings.js @@ -19,6 +19,8 @@ routes.push({ path: '/settings', name: 'settings', component: { email: null, password: null, csrfToken: null, + method: null, + email: null, }, rules: { required: value => !!value || this.$root.i18n.required, @@ -31,7 +33,7 @@ routes.push({ path: '/settings', name: 'settings', component: { this.reloadSettings(); } else { this.showSettingsForm = true; - this.authSettingsUrl = this.$root.authUrl + 'settings/methods/password' + location.search; + this.authSettingsUrl = this.$root.authUrl + 'settings' + location.search; this.loadData() } this.usingDefaults = localStorage.length == 0; @@ -49,15 +51,15 @@ routes.push({ path: '/settings', name: 'settings', component: { async loadData() { try { const response = await this.$root.authApi.get('settings/flows?id=' + this.$root.getAuthFlowId()); - this.form.csrfToken = response.data.methods.password.config.fields.find(item => item.name == 'csrf_token').value; + this.form.csrfToken = response.data.ui.nodes.find(item => item.attributes && item.attributes.name == 'csrf_token').attributes.value; + this.form.method = "password"; var errors = []; - response.data.methods.password.config.fields.forEach(function(value, index, array) { - if (value.messages) { - value.messages.forEach(function(err, idx, errArray) { - errors.push(err.text); - }); + if (response.data.ui.messages) { + const error = response.data.ui.messages.find(item => item.type == "error"); + if (error && error.text) { + errors.push(error.text); } - }); + } if (errors.length > 0) { this.$root.showWarning(this.i18n.settingsInvalid + errors.join("\n")); } else if (response.data.state == "success") { diff --git a/html/login/index.html b/html/login/index.html index 5c48aae3e..6b20e4ef2 100644 --- a/html/login/index.html +++ b/html/login/index.html @@ -60,9 +60,10 @@ - + + diff --git a/server/modules/kratos/kratosuser.go b/server/modules/kratos/kratosuser.go index f64378271..f30e04c88 100644 --- a/server/modules/kratos/kratosuser.go +++ b/server/modules/kratos/kratosuser.go @@ -19,7 +19,6 @@ type KratosTraits struct { LastName string `json:"lastName"` Role string `json:"role"` Status string `json:"status"` - SearchUsername string `json:"searchUsername"` } func NewTraits(email string, firstName string, lastName string, role string, status string) *KratosTraits { @@ -78,7 +77,6 @@ func (kratosUser* KratosUser) copyToUser(user *model.User) { user.LastName = kratosUser.Traits.LastName user.Role = kratosUser.Traits.Role user.Status = kratosUser.Traits.Status - user.SearchUsername = kratosUser.Traits.SearchUsername } func (kratosUser* KratosUser) copyFromUser(user *model.User) { @@ -90,7 +88,6 @@ func (kratosUser* KratosUser) copyFromUser(user *model.User) { kratosUser.Traits.LastName = user.LastName kratosUser.Traits.Role = user.Role kratosUser.Traits.Status = user.Status - kratosUser.Traits.SearchUsername = user.SearchUsername if len(kratosUser.Addresses) == 0 { kratosUser.Addresses = make([]*KratosAddress, 1) kratosUser.Addresses[0] = &KratosAddress{} diff --git a/server/modules/kratos/kratosuser_test.go b/server/modules/kratos/kratosuser_test.go index cee708111..a91649e4a 100644 --- a/server/modules/kratos/kratosuser_test.go +++ b/server/modules/kratos/kratosuser_test.go @@ -38,9 +38,6 @@ func TestCopyFromUser(tester *testing.T) { if kratosUser.Traits.Status != user.Status { tester.Errorf("Status failed to convert") } - if kratosUser.Traits.SearchUsername != user.SearchUsername { - tester.Errorf("SearchUsername failed to convert") - } if kratosUser.Addresses[0].Value != user.Email { tester.Errorf("Address failed to convert") } @@ -48,7 +45,6 @@ func TestCopyFromUser(tester *testing.T) { func TestCopyToUser(tester *testing.T) { kratosUser := NewKratosUser("myEmail", "myFirst", "myLast", "myRole", "locked") - kratosUser.Traits.SearchUsername = "mysearchuser" user := model.NewUser() kratosUser.copyToUser(user) if kratosUser.Traits.Email != user.Email { @@ -66,9 +62,6 @@ func TestCopyToUser(tester *testing.T) { if kratosUser.Traits.Status != user.Status { tester.Errorf("Status failed to convert") } - if kratosUser.Traits.SearchUsername != user.SearchUsername { - tester.Errorf("SearchUsername failed to convert") - } if kratosUser.Addresses[0].Value != user.Email { tester.Errorf("Address failed to convert") } From 09dff5e4871069b60fdfc6d592a9f2bdd59623a9 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Tue, 25 May 2021 08:12:47 -0400 Subject: [PATCH 2/7] Remove unused email form field from settings --- html/js/routes/settings.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/html/js/routes/settings.js b/html/js/routes/settings.js index 957c603e3..4b841bff5 100644 --- a/html/js/routes/settings.js +++ b/html/js/routes/settings.js @@ -16,11 +16,9 @@ routes.push({ path: '/settings', name: 'settings', component: { usingDefaults: false, form: { valid: false, - email: null, password: null, csrfToken: null, method: null, - email: null, }, rules: { required: value => !!value || this.$root.i18n.required, From 9f7348cc698c667dee39ef073ed95e617efe845e Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Wed, 9 Jun 2021 16:39:39 -0400 Subject: [PATCH 3/7] Add username/email to top of profile menu for quick identification of logged in user --- html/index.html | 6 ++++++ html/js/app.js | 7 +++++++ model/info.go | 1 + server/infohandler.go | 21 ++++++++++++++------- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/html/index.html b/html/index.html index d561400d8..a2ae1045e 100644 --- a/html/index.html +++ b/html/index.html @@ -131,6 +131,12 @@ + + + + + + fa-adjust diff --git a/html/js/app.js b/html/js/app.js index 414d9ee5c..a207c8195 100644 --- a/html/js/app.js +++ b/html/js/app.js @@ -80,6 +80,8 @@ $(document).ready(function() { usersLoadedDate: null, cacheRefreshIntervalMs: 300000, loadServerSettingsTime: 0, + user: null, + username: '', }, watch: { '$vuetify.theme.dark': 'saveLocalSettings', @@ -268,6 +270,11 @@ $(document).ready(function() { this.elasticVersion = response.data.elasticVersion; this.wazuhVersion = response.data.wazuhVersion; + this.user = await this.getUserById(response.data.userId); + if (this.user) { + this.username = this.user.email; + } + if (this.parameterCallback != null) { this.parameterCallback(this.parameters[this.parameterSection]); this.parameterCallback = null; diff --git a/model/info.go b/model/info.go index 196fe1bde..613e8aaab 100644 --- a/model/info.go +++ b/model/info.go @@ -20,4 +20,5 @@ type Info struct { Parameters *config.ClientParameters `json:"parameters"` ElasticVersion string `json:"elasticVersion"` WazuhVersion string `json:"wazuhVersion"` + UserId string `json:"userId"` } diff --git a/server/infohandler.go b/server/infohandler.go index fbbdc2e33..084484dc2 100644 --- a/server/infohandler.go +++ b/server/infohandler.go @@ -40,12 +40,19 @@ func (infoHandler *InfoHandler) HandleNow(ctx context.Context, writer http.Respo } func (infoHandler *InfoHandler) get(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { - info := &model.Info{ - Version: infoHandler.Host.Version, - License: "GPL v2", - Parameters: &infoHandler.server.Config.ClientParams, - ElasticVersion: os.Getenv("ELASTIC_VERSION"), - WazuhVersion: os.Getenv("WAZUH_VERSION"), + var err error + var info *model.Info + if user, ok := request.Context().Value(web.ContextKeyRequestor).(*model.User); ok { + info = &model.Info{ + Version: infoHandler.Host.Version, + License: "GPL v2", + Parameters: &infoHandler.server.Config.ClientParams, + ElasticVersion: os.Getenv("ELASTIC_VERSION"), + WazuhVersion: os.Getenv("WAZUH_VERSION"), + UserId: user.Id, + } + } else { + err = errors.New("Unable to determine logged in user from context") } - return http.StatusOK, info, nil + return http.StatusOK, info, err } From d6f883d35c4c5d4cf0ffc7914d98d318ef0feae4 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Thu, 10 Jun 2021 15:20:33 -0400 Subject: [PATCH 4/7] Provide time range when perform PCAP pivots to eliminate full index search --- html/js/app.js | 2 +- server/modules/elastic/elasticeventstore.go | 71 +++++++++++++++------ server/modules/elastic/joblookuphandler.go | 18 +++--- 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/html/js/app.js b/html/js/app.js index a207c8195..243615d4f 100644 --- a/html/js/app.js +++ b/html/js/app.js @@ -216,7 +216,7 @@ $(document).ready(function() { }, getDynamicActionFieldNames(url) { const fields = []; - const matches = url.matchAll(/\{:([a-zA-Z0-9_.]+?)(\|.*?)?\}/g); + const matches = url.matchAll(/\{:([@a-zA-Z0-9_.]+?)(\|.*?)?\}/g); for (const match of matches) { if (match.length > 1) { fields.push(match[1]); diff --git a/server/modules/elastic/elasticeventstore.go b/server/modules/elastic/elasticeventstore.go index 67aa05fa1..7709788ea 100644 --- a/server/modules/elastic/elasticeventstore.go +++ b/server/modules/elastic/elasticeventstore.go @@ -435,6 +435,22 @@ func (store *ElasticEventstore) parseFirst(json string, name string) string { return result } +func (store *ElasticEventstore) buildRangeFilter(timestampStr string) (string, time.Time) { + if len(timestampStr) > 0 { + timestamp, err := time.Parse(time.RFC3339, timestampStr) + if err != nil { + log.WithFields(log.Fields { + "timestampStr": timestampStr, + }).WithError(err).Error("Unable to parse document timestamp") + } + startTime := timestamp.Add(time.Duration(-store.esSearchOffsetMs) * time.Millisecond).Unix() * 1000 + endTime := timestamp.Add(time.Duration(store.esSearchOffsetMs) * time.Millisecond).Unix() * 1000 + filter := fmt.Sprintf(`,{"range":{"@timestamp":{"gte":"%d","lte":"%d","format":"epoch_millis"}}}`, startTime, endTime) + return filter, timestamp + } + return "", time.Time{} +} + /** * Fetch record via provided Elasticsearch document query. * If the record has a tunnel_parent, search for a UID=tunnel_parent[0] @@ -447,7 +463,20 @@ func (store *ElasticEventstore) parseFirst(json string, name string) string { * Review the results from the Zeek search and find the record with the timestamp nearest to the original ES ID record and use the IP/port details as the filter. */ -func (store *ElasticEventstore) PopulateJobFromDocQuery(ctx context.Context, query string, job *model.Job) error { +func (store *ElasticEventstore) PopulateJobFromDocQuery(ctx context.Context, idField string, idValue string, timestampStr string, job *model.Job) error { + rangeFilter, timestamp := store.buildRangeFilter(timestampStr) + + query := fmt.Sprintf(` + { + "query" : { + "bool": { + "must": [ + { "match" : { "%s" : "%s" }}%s + ] + } + } + }`, idField, idValue, rangeFilter) + var outputSensorId string filter := model.NewFilter() json, err := store.luceneSearch(ctx, query) @@ -467,6 +496,12 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(ctx context.Context, que return errors.New("Unable to locate document record") } + // Try to grab the timestamp from this new record, if the time wasn't provided to this function + if len(rangeFilter) == 0 { + timestampStr = gjson.Get(json, "hits.hits.0._source.\\@timestamp").String() + rangeFilter, timestamp = store.buildRangeFilter(timestampStr) + } + // Check if user has pivoted to a PCAP that is encapsulated in a tunnel. The best we // can do in this situation is respond with the tunnel PCAP data, which could be excessive. tunnelParent := gjson.Get(json, "hits.hits.0._source.log.id.tunnel_parents").String() @@ -475,7 +510,17 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(ctx context.Context, que if tunnelParent[0] == '[' { tunnelParent = gjson.Get(json, "hits.hits.0._source.log.id.tunnel_parents.0").String() } - query := fmt.Sprintf(`{"query" : { "bool": { "must": { "match" : { "log.id.uid" : "%s" }}}}}`, tunnelParent) + query := fmt.Sprintf(` + { + "query" : { + "bool": { + "must": [ + { "match" : { "log.id.uid" : "%s" }}%s + ] + } + } + }`, tunnelParent, rangeFilter) + json, err = store.luceneSearch(ctx, query) log.WithFields(log.Fields{ "query": query, @@ -492,17 +537,6 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(ctx context.Context, que } } - timestampStr := gjson.Get(json, "hits.hits.0._source.\\@timestamp").String() - var timestamp time.Time - timestamp, err = time.Parse(time.RFC3339, timestampStr) - if err != nil { - log.WithFields(log.Fields { - "query": query, - "timestamp": timestamp, - }).WithError(err).Error("Unable to parse document timestamp") - return err - } - filter.ImportId = gjson.Get(json, "hits.hits.0._source.import.id").String() filter.SrcIp = gjson.Get(json, "hits.hits.0._source.source.ip").String() filter.SrcPort = int(gjson.Get(json, "hits.hits.0._source.source.port").Int()) @@ -516,9 +550,6 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(ctx context.Context, que // If source and destination IP/port details aren't available search ES again for a correlating Zeek record if len(filter.SrcIp) == 0 || len(filter.DstIp) == 0 || filter.SrcPort == 0 || filter.DstPort == 0 { - startTime := timestamp.Add(time.Duration(-store.esSearchOffsetMs) * time.Millisecond).Unix() * 1000 - endTime := timestamp.Add(time.Duration(store.esSearchOffsetMs) * time.Millisecond).Unix() * 1000 - if len(uid) == 0 || uid[0] != 'C' { zeekFileQuery := "" if len(x509id) > 0 && x509id[0] == 'F' { @@ -528,8 +559,8 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(ctx context.Context, que } if len(zeekFileQuery) > 0 { - query = fmt.Sprintf(`{"query":{"bool":{"must":[{"query_string":{"query":"event.module:zeek AND event.dataset:file AND %s","analyze_wildcard":true}},{"range":{"@timestamp":{"gte":"%d","lte":"%d","format":"epoch_millis"}}}]}}}`, - zeekFileQuery, startTime, endTime) + query = fmt.Sprintf(`{"query":{"bool":{"must":[{"query_string":{"query":"event.module:zeek AND event.dataset:file AND %s","analyze_wildcard":true}}%s]}}}`, + zeekFileQuery, rangeFilter) json, err = store.luceneSearch(ctx, query) log.WithFields(log.Fields{ "query": query, @@ -570,8 +601,8 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(ctx context.Context, que } // Search for the Zeek connection ID - query = fmt.Sprintf(`{"query":{"bool":{"must":[{"query_string":{"query":"event.module:zeek AND %s","analyze_wildcard":true}},{"range":{"@timestamp":{"gte":"%d","lte":"%d","format":"epoch_millis"}}}]}}}`, - uid, startTime, endTime) + query = fmt.Sprintf(`{"query":{"bool":{"must":[{"query_string":{"query":"event.module:zeek AND %s","analyze_wildcard":true}}%s]}}}`, + uid, rangeFilter) json, err = store.luceneSearch(ctx, query) log.WithFields(log.Fields{ "query": query, diff --git a/server/modules/elastic/joblookuphandler.go b/server/modules/elastic/joblookuphandler.go index 4152a2ed1..aa4558e5c 100644 --- a/server/modules/elastic/joblookuphandler.go +++ b/server/modules/elastic/joblookuphandler.go @@ -13,7 +13,6 @@ package elastic import ( "context" "errors" - "fmt" "net/http" "strconv" "github.com/security-onion-solutions/securityonion-soc/model" @@ -45,17 +44,18 @@ func (handler *JobLookupHandler) HandleNow(ctx context.Context, writer http.Resp func (handler *JobLookupHandler) get(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { statusCode := http.StatusBadRequest - esId := request.URL.Query().Get("esid") // Elastic doc ID - var query string - if len(esId) > 0 { - query = fmt.Sprintf(`{"query" : { "bool": { "must": { "match" : { "_id" : "%s" }}}}}`, esId) - } else { - ncId := request.URL.Query().Get("ncid") // Network community ID - query = fmt.Sprintf(`{"query" : { "bool": { "must": { "match" : { "network.community_id" : "%s" }}}}}`, ncId) + + timestampStr := request.URL.Query().Get("time") // Elastic doc timestamp + + idField := "_id" + idValue := request.URL.Query().Get("esid") // Elastic doc ID + if len(idValue) == 0 { + idValue = request.URL.Query().Get("ncid") // Network community ID + idField = "network.community_id" } job := handler.server.Datastore.CreateJob() - err := handler.store.PopulateJobFromDocQuery(ctx, query, job) + err := handler.store.PopulateJobFromDocQuery(ctx, idField, idValue, timestampStr, job) if err == nil { if user, ok := ctx.Value(web.ContextKeyRequestor).(*model.User); ok { job.UserId = user.Id From 403e3abce216540912fd08fb532ac5753dff46b2 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Thu, 10 Jun 2021 17:23:03 -0400 Subject: [PATCH 5/7] Allow adjusting the timezone in the hunt interface --- config/serverconfig.go | 4 ++++ config/serverconfig_test.go | 3 +++ html/index.html | 36 ++++++++++++++++++++++-------------- html/js/app.js | 1 + html/js/i18n.js | 2 ++ html/js/routes/hunt.js | 7 +++++++ html/js/routes/hunt.test.js | 8 ++++++++ html/js/test_common.js | 1 + model/info.go | 1 + scripts/timezones.sh | 4 ++++ server/infohandler.go | 3 +++ server/server.go | 18 ++++++++++++++++++ 12 files changed, 74 insertions(+), 14 deletions(-) create mode 100755 scripts/timezones.sh diff --git a/config/serverconfig.go b/config/serverconfig.go index aa2d8ff6d..18d0a7ece 100644 --- a/config/serverconfig.go +++ b/config/serverconfig.go @@ -29,6 +29,7 @@ type ServerConfig struct { ModuleFailuresIgnored bool `json:"moduleFailuresIgnored"` ClientParams ClientParameters `json:"client"` IdleConnectionTimeoutMs int `json:"idleConnectionTimeoutMs"` + TimezoneScript string `json:"timezoneScript"` } func (config *ServerConfig) Verify() error { @@ -51,5 +52,8 @@ func (config *ServerConfig) Verify() error { if (config.IdleConnectionTimeoutMs <= 0) { config.IdleConnectionTimeoutMs = DEFAULT_IDLE_CONNECTION_TIMEOUT_MS } + if len(config.TimezoneScript) == 0 { + config.TimezoneScript = "/opt/sensoroni/scripts/timezones.sh" + } return err } \ No newline at end of file diff --git a/config/serverconfig_test.go b/config/serverconfig_test.go index b5f415a80..11f174266 100644 --- a/config/serverconfig_test.go +++ b/config/serverconfig_test.go @@ -36,4 +36,7 @@ func TestVerifyServer(tester *testing.T) { if err != nil { tester.Errorf("expected no error") } + if cfg.TimezoneScript != "/opt/sensoroni/scripts/timezones.sh" { + tester.Errorf("Unexpected default timezone script: %d", cfg.TimezoneScript) + } } diff --git a/html/index.html b/html/index.html index 552335bff..b868fca9f 100644 --- a/html/index.html +++ b/html/index.html @@ -261,23 +261,31 @@

- - - - - - + + + + {{i18n.options}} + + + + + + + + - + +

{{ i18n.eventTotal }} {{ totalEvents.toLocaleString() }}

-

{{ i18n.timezone }} {{ zone }}
+
+
diff --git a/html/js/app.js b/html/js/app.js index 243615d4f..a8dcd1dfb 100644 --- a/html/js/app.js +++ b/html/js/app.js @@ -269,6 +269,7 @@ $(document).ready(function() { this.parameters = response.data.parameters; this.elasticVersion = response.data.elasticVersion; this.wazuhVersion = response.data.wazuhVersion; + this.timezones = response.data.timezones; this.user = await this.getUserById(response.data.userId); if (this.user) { diff --git a/html/js/i18n.js b/html/js/i18n.js index 085b5d798..50d492761 100644 --- a/html/js/i18n.js +++ b/html/js/i18n.js @@ -209,6 +209,7 @@ const i18n = { ok: 'OK', offline: 'Offline', online: 'Online', + options: 'Options', owner: 'Owner', packages: 'Packages', packets: 'Captured Packets', @@ -285,6 +286,7 @@ const i18n = { timestamp: 'Timestamp', timestampFormat: 'YYYY-MM-DD HH:mm:ss.SSS Z', timezone: 'Time Zone:', + timezoneHelp: 'Time Zone', toolCyberchef: 'CyberChef', toolCyberchefHelp: 'Data decoding and transformation tools', toolFleet: 'FleetDM', diff --git a/html/js/routes/hunt.js b/html/js/routes/hunt.js index 087a8d1d9..02328651b 100644 --- a/html/js/routes/hunt.js +++ b/html/js/routes/hunt.js @@ -1058,6 +1058,9 @@ const huntComponent = { localStorage.removeItem(item); } }, + saveTimezone() { + localStorage['timezone'] = this.zone; + }, saveLocalSettings() { this.saveSetting('groupBySortBy', this.groupBySortBy, 'timestamp'); this.saveSetting('groupBySortDesc', this.groupBySortDesc, true); @@ -1073,6 +1076,10 @@ const huntComponent = { this.saveSetting('autohunt', this.autohunt, true); }, loadLocalSettings() { + // Global settings + if (localStorage['timezone']) this.zone = localStorage['timezone']; + + // Module settings var prefix = 'settings.' + this.category; if (localStorage[prefix + '.groupBySortBy']) this.groupBySortBy = localStorage[prefix + '.groupBySortBy']; if (localStorage[prefix + '.groupBySortDesc']) this.groupBySortDesc = localStorage[prefix + '.groupBySortDesc'] == "true"; diff --git a/html/js/routes/hunt.test.js b/html/js/routes/hunt.test.js index f81f38c9f..e2b432562 100644 --- a/html/js/routes/hunt.test.js +++ b/html/js/routes/hunt.test.js @@ -117,3 +117,11 @@ test('sortAlertsAscending', () => { expect(sorted[1]).toBe(item3); expect(sorted[0]).toBe(item4); }); + +test('saveTimezone', () => { + comp.zone = "Foo/Bar"; + comp.saveTimezone(); + comp.zone = "Test"; + comp.loadLocalSettings(); + expect(comp.zone).toBe("Foo/Bar"); +}); \ No newline at end of file diff --git a/html/js/test_common.js b/html/js/test_common.js index 652144726..00da420ed 100644 --- a/html/js/test_common.js +++ b/html/js/test_common.js @@ -14,6 +14,7 @@ global.document = {}; global.navigator = {}; global.location = {}; +global.localStorage = {}; global.btoa = function(content) { return Buffer.from(content, 'binary').toString('base64'); }; diff --git a/model/info.go b/model/info.go index 613e8aaab..7d90348af 100644 --- a/model/info.go +++ b/model/info.go @@ -21,4 +21,5 @@ type Info struct { ElasticVersion string `json:"elasticVersion"` WazuhVersion string `json:"wazuhVersion"` UserId string `json:"userId"` + Timezones []string `json:"timezones"` } diff --git a/scripts/timezones.sh b/scripts/timezones.sh new file mode 100755 index 000000000..cccf1ed6c --- /dev/null +++ b/scripts/timezones.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +cd /usr/share/zoneinfo +find -type f | cut -c 3- | sort | grep -v ".tab\|tzdata\|right\/\|SystemV\/\|posix\/\|localtime\|Factory\|seconds\|posixrules" \ No newline at end of file diff --git a/server/infohandler.go b/server/infohandler.go index 084484dc2..a2b87c147 100644 --- a/server/infohandler.go +++ b/server/infohandler.go @@ -22,6 +22,7 @@ import ( type InfoHandler struct { web.BaseHandler server *Server + timezones []string } func NewInfoHandler(srv *Server) *InfoHandler { @@ -29,6 +30,7 @@ func NewInfoHandler(srv *Server) *InfoHandler { handler.Host = srv.Host handler.server = srv handler.Impl = handler + handler.timezones = srv.GetTimezones() return handler } @@ -50,6 +52,7 @@ func (infoHandler *InfoHandler) get(ctx context.Context, writer http.ResponseWri ElasticVersion: os.Getenv("ELASTIC_VERSION"), WazuhVersion: os.Getenv("WAZUH_VERSION"), UserId: user.Id, + Timezones: infoHandler.timezones, } } else { err = errors.New("Unable to determine logged in user from context") diff --git a/server/server.go b/server/server.go index 4e28cd9fa..77b442ace 100644 --- a/server/server.go +++ b/server/server.go @@ -11,6 +11,8 @@ package server import ( + "os/exec" + "strings" "github.com/apex/log" "github.com/security-onion-solutions/securityonion-soc/config" "github.com/security-onion-solutions/securityonion-soc/web" @@ -70,3 +72,19 @@ func (server *Server) Stop() { func (server *Server) Wait() { <- server.stoppedChan } + +func (server *Server) GetTimezones() []string { + var zones []string = make([]string, 0, 0) + bytes, err := exec.Command(server.Config.TimezoneScript).Output() + if err == nil { + output := string(bytes) + if strings.Contains(output, "America/New_York") { + zones = strings.Split(output, "\n") + } else { + log.WithError(err).Error("Timezone output is invalid") + } + } else { + log.WithError(err).Error("Unable to lookup timezones from operating system") + } + return zones +} \ No newline at end of file From 73c4945ac738247e216bf0a4b217181fa2210a1d Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Thu, 10 Jun 2021 17:29:32 -0400 Subject: [PATCH 6/7] Correct unit test --- config/serverconfig_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/serverconfig_test.go b/config/serverconfig_test.go index 11f174266..44b04178b 100644 --- a/config/serverconfig_test.go +++ b/config/serverconfig_test.go @@ -37,6 +37,6 @@ func TestVerifyServer(tester *testing.T) { tester.Errorf("expected no error") } if cfg.TimezoneScript != "/opt/sensoroni/scripts/timezones.sh" { - tester.Errorf("Unexpected default timezone script: %d", cfg.TimezoneScript) + tester.Errorf("Unexpected default timezone script: %s", cfg.TimezoneScript) } } From 4119a6b7fe2b68f317d18e7ee43bf928b8624e6c Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Thu, 10 Jun 2021 21:18:49 -0400 Subject: [PATCH 7/7] Add id attr for options expander --- html/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/index.html b/html/index.html index b868fca9f..c4f36a714 100644 --- a/html/index.html +++ b/html/index.html @@ -264,7 +264,7 @@

- {{i18n.options}} + {{i18n.options}}