From 04734b4bd8f5a32cfa18eb4f82ff3e6a565354ff Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Tue, 4 May 2021 17:24:49 -0400 Subject: [PATCH 01/29] Prevent red error banner from briefly appearing after logging into SOC --- html/js/app.js | 62 ++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/html/js/app.js b/html/js/app.js index 294febda4..3d835f6ef 100644 --- a/html/js/app.js +++ b/html/js/app.js @@ -122,40 +122,42 @@ $(document).ready(function() { this.loadServerSettingsTime = now; try { const response = await this.papi.get('info'); - this.version = response.data.version; - this.license = response.data.license; - this.parameters = response.data.parameters; - this.elasticVersion = response.data.elasticVersion; - this.wazuhVersion = response.data.wazuhVersion; + if (response) { + this.version = response.data.version; + this.license = response.data.license; + this.parameters = response.data.parameters; + this.elasticVersion = response.data.elasticVersion; + this.wazuhVersion = response.data.wazuhVersion; - if (this.parameterCallback != null) { - this.parameterCallback(this.parameters[this.parameterSection]); - this.parameterCallback = null; - } - this.parametersLoaded = true; - if (this.parameters.webSocketTimeoutMs > 0) { - this.wsConnectionTimeout = this.parameters.webSocketTimeoutMs; - } - if (this.parameters.apiTimeoutMs > 0) { - this.connectionTimeout = this.parameters.apiTimeoutMs; - } - if (this.parameters.cacheExpirationMs > 0) { - this.cacheRefreshIntervalMs = this.parameters.cacheExpirationMs; - } - if (this.parameters.tipTimeoutMs > 0) { - this.tipTimeout = this.parameters.tipTimeoutMs; - } - if (this.parameters.tools && this.parameters.tools.length > 0) { - this.tools = this.parameters.tools; - if (this.parameters.inactiveTools) { - const inactive = this.parameters.inactiveTools; - for (var i = 0; i < this.tools.length; i++) { - const tool = this.tools[i]; - tool.enabled = !inactive.includes(tool.name); + if (this.parameterCallback != null) { + this.parameterCallback(this.parameters[this.parameterSection]); + this.parameterCallback = null; + } + this.parametersLoaded = true; + if (this.parameters.webSocketTimeoutMs > 0) { + this.wsConnectionTimeout = this.parameters.webSocketTimeoutMs; + } + if (this.parameters.apiTimeoutMs > 0) { + this.connectionTimeout = this.parameters.apiTimeoutMs; + } + if (this.parameters.cacheExpirationMs > 0) { + this.cacheRefreshIntervalMs = this.parameters.cacheExpirationMs; + } + if (this.parameters.tipTimeoutMs > 0) { + this.tipTimeout = this.parameters.tipTimeoutMs; + } + if (this.parameters.tools && this.parameters.tools.length > 0) { + this.tools = this.parameters.tools; + if (this.parameters.inactiveTools) { + const inactive = this.parameters.inactiveTools; + for (var i = 0; i < this.tools.length; i++) { + const tool = this.tools[i]; + tool.enabled = !inactive.includes(tool.name); + } } } + this.subscribe("status", this.updateStatus); } - this.subscribe("status", this.updateStatus); } catch (error) { if (!background) { // Only show the error on initial startup, otherwise the error From 7dbcc12e34361dd11ac3a0f52eadc07b1a3759c1 Mon Sep 17 00:00:00 2001 From: Doug Burks Date: Fri, 7 May 2021 13:18:23 -0400 Subject: [PATCH 02/29] FEATURE: Pivot from Alerts/Hunt to CyberChef https://github.com/Security-Onion-Solutions/securityonion/issues/4081 --- html/js/i18n.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/html/js/i18n.js b/html/js/i18n.js index 6c732f94d..9926fa599 100644 --- a/html/js/i18n.js +++ b/html/js/i18n.js @@ -23,6 +23,8 @@ const i18n = { actionAlertHelp: 'Create an alert for this event', actionCorrelate: 'Correlate', actionCorrelateHelp: 'Show related events', + actionCyberChef: 'CyberChef', + actionCyberChefHelp: 'Analyze this field using CyberChef', actionFailure: 'Action failed (check browser console for more error details): ', actionGoogle: 'Google', actionGoogleHelp: 'Perform a Google search on this field', @@ -330,4 +332,4 @@ const i18n = { }, } -if (typeof global !== 'undefined') global.i18n = i18n; \ No newline at end of file +if (typeof global !== 'undefined') global.i18n = i18n; From 427bf120735333743419ec9e98bc57f9272f3881 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Wed, 12 May 2021 11:59:18 -0400 Subject: [PATCH 03/29] Refactor for improved Elastic integration --- go.mod | 1 + go.sum | 51 +++++++++ model/user.go | 6 + server/casehandler.go | 7 +- server/eventhandler.go | 15 +-- server/eventstore.go | 7 +- server/gridhandler.go | 7 +- server/infohandler.go | 7 +- server/jobhandler.go | 33 +++--- server/jobshandler.go | 7 +- server/modules/elastic/elasticeventstore.go | 107 +++++++++++------- server/modules/elastic/elastictransport.go | 61 ++++++++++ .../modules/elastic/elastictransport_test.go | 61 ++++++++++ server/modules/elastic/joblookuphandler.go | 28 +++-- server/modules/kratos/kratos.go | 6 +- server/modules/kratos/kratosuser.go | 31 ++--- server/modules/kratos/kratosuser_test.go | 15 ++- server/modules/kratos/kratosuserstore.go | 61 ++++++---- server/modules/kratos/kratosuserstore_test.go | 2 +- server/modules/statickeyauth/statickeyauth.go | 2 +- .../statickeyauth/statickeyauth_test.go | 7 +- .../statickeyauth/statickeyauthimpl.go | 24 +++- .../statickeyauth/statickeyauthimpl_test.go | 36 ++++++ server/nodehandler.go | 7 +- server/packethandler.go | 7 +- server/queryhandler.go | 7 +- server/streamhandler.go | 11 +- server/userhandler.go | 9 +- server/usershandler.go | 9 +- web/basehandler.go | 77 ++++++------- web/basehandler_test.go | 14 --- web/basepreprocessor.go | 39 +++++++ web/basepreprocessor_test.go | 39 +++++++ web/host.go | 87 ++++++++++++-- web/host_test.go | 71 ++++++++++++ web/websockethandler.go | 3 +- 36 files changed, 737 insertions(+), 225 deletions(-) create mode 100644 server/modules/elastic/elastictransport.go create mode 100644 server/modules/elastic/elastictransport_test.go create mode 100644 web/basepreprocessor.go create mode 100644 web/basepreprocessor_test.go diff --git a/go.mod b/go.mod index 9277d1802..4c9365aa6 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/apex/log v1.9.0 github.com/elastic/go-elasticsearch/v7 v7.11.0 github.com/google/gopacket v1.1.19 + github.com/google/uuid v1.1.1 // indirect github.com/gorilla/websocket v1.4.2 github.com/influxdata/influxdb-client-go/v2 v2.2.3 // indirect github.com/kennygrant/sanitize v1.2.4 diff --git a/go.sum b/go.sum index 5cd0bc217..9822cd3ed 100644 --- a/go.sum +++ b/go.sum @@ -1,61 +1,90 @@ github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= +github.com/apex/logs v1.0.0 h1:adOwhOTeXzZTnVuEK13wuJNBFutP0sOfutRS8NY+G6A= github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= +github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a h1:2KLQMJ8msqoPHIPDufkxVcoTtcmE5+1sL9950m4R9Pk= github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= +github.com/aphistic/sweet v0.2.0 h1:I4z+fAUqvKfvZV/CHi5dV0QuwbmIvYYFDjG0Ss5QpAs= github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= +github.com/aws/aws-sdk-go v1.20.6 h1:kmy4Gvdlyez1fV4kw5RYxZzWKVyuHZHgPWeU/YvRsV4= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c h1:/ovYnF02fwL0kvspmy9AuyKg1JhdTRUgPw4nUxd9oZM= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deepmap/oapi-codegen v1.3.13 h1:9HKGCsdJqE4dnrQ8VerFS0/1ZOJPmAhN+g8xgp8y3K4= github.com/deepmap/oapi-codegen v1.3.13/go.mod h1:WAmG5dWY8/PYHt4vKxlt90NsbHMAOCiteYKZMiIRfOo= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/elastic/go-elasticsearch/v7 v7.11.0 h1:bv+2GqsVrPdX/ChJqAHAFtWgtGvVJ0icN/WdBGAdNuw= github.com/elastic/go-elasticsearch/v7 v7.11.0/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getkin/kin-openapi v0.13.0 h1:03fqBEEgivp4MVK2ElB140B56hjO9ZFvFTHBsvFsSro= github.com/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/influxdata/influxdb-client-go/v2 v2.2.3 h1:082jdJ5t1CFeo0rpGQvKAK1mONVSbFhL4finWA5bRM8= github.com/influxdata/influxdb-client-go/v2 v2.2.3/go.mod h1:fa/d1lAdUHxuc1jedx30ZfNG573oQTQmUni3N6pcW+0= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME= github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/labstack/echo/v4 v4.1.11 h1:z0BZoArY4FqdpUEl+wlHp4hnr/oSR6MTmQmv8OHSoww= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= +github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd h1:HvFwW+cm9bCbZ/+vuGNq7CRWXql8c0y8nGeYpqmpvmk= github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -63,11 +92,17 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/fastuuid v1.1.0 h1:INyGLmTCMGFr6OVIb977ghJvABML2CMVjPoRfNDdYDo= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= +github.com/smartystreets/gunit v1.0.0 h1:RyPDUFcJbvtXlhJPk7v+wnxZRY2EUokhEYl2EJOPToI= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -83,19 +118,28 @@ github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= +github.com/tj/go-buffer v1.1.0 h1:Lo2OsPHlIxXF24zApe15AbK3bJLAOvkkxEA6Ux4c47M= github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc= +github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2 h1:eGaGNxrtoZf/mBURsnNQKDR7u50Klgcf2eFDQEnc8Bc= github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= +github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b h1:m74UWYy+HBs+jMFR9mdZU6shPewugMyH5+GV6LNgW8w= github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= +github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds= github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 h1:pXVtWnwHkrWD9ru3sDxY/qFK/bfc0egRovX91EjWjf4= golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -104,6 +148,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20191112182307-2180aed22343 h1:00ohfJ4K98s3m6BGUoBd8nyfp4Yl0GoIKvw5abItTjI= golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -115,16 +160,22 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191115151921-52ab43148777 h1:wejkGHRTr38uaKRqECZlsCsJ1/TGxIyFbH32x5zUdu4= golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/model/user.go b/model/user.go index 530513582..41330f241 100644 --- a/model/user.go +++ b/model/user.go @@ -23,6 +23,7 @@ type User struct { LastName string `json:"lastName"` Role string `json:"role"` Status string `json:"status"` + SearchUsername string `json:"searchUsername"` } func NewUser() *User { @@ -32,5 +33,10 @@ func NewUser() *User { FirstName: "", LastName: "", Status: "", + SearchUsername: "", } } + +func (user *User) String() string { + return user.Id +} diff --git a/server/casehandler.go b/server/casehandler.go index 2bf13bb06..d8a9e280e 100644 --- a/server/casehandler.go +++ b/server/casehandler.go @@ -10,6 +10,7 @@ package server import ( + "context" "errors" "encoding/json" "net/http" @@ -30,20 +31,20 @@ func NewCaseHandler(srv *Server) *CaseHandler { return handler } -func (caseHandler *CaseHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (caseHandler *CaseHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { if caseHandler.server.Casestore == nil { return http.StatusMethodNotAllowed, nil, errors.New("CASE_MODULE_NOT_ENABLED") } if caseHandler.server.Casestore != nil { switch request.Method { - case http.MethodPost: return caseHandler.create(writer, request) + case http.MethodPost: return caseHandler.create(ctx, writer, request) } } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } -func (caseHandler *CaseHandler) create(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (caseHandler *CaseHandler) create(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { statusCode := http.StatusBadRequest var outputCase *model.Case diff --git a/server/eventhandler.go b/server/eventhandler.go index 60041969f..93658d099 100644 --- a/server/eventhandler.go +++ b/server/eventhandler.go @@ -10,6 +10,7 @@ package server import ( + "context" "errors" "encoding/json" "net/http" @@ -30,21 +31,21 @@ func NewEventHandler(srv *Server) *EventHandler { return handler } -func (eventHandler *EventHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (eventHandler *EventHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { if eventHandler.server.Eventstore != nil { switch request.Method { - case http.MethodGet: return eventHandler.get(writer, request) + case http.MethodGet: return eventHandler.get(ctx, writer, request) case http.MethodPost: obj := eventHandler.GetPathParameter(request.URL.Path, 2) if obj == "ack" { - return eventHandler.ack(writer, request) + return eventHandler.ack(ctx, writer, request) } } } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } -func (eventHandler *EventHandler) get(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (eventHandler *EventHandler) get(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { var results *model.EventSearchResults statusCode := http.StatusBadRequest @@ -58,7 +59,7 @@ func (eventHandler *EventHandler) get(writer http.ResponseWriter, request *http. request.Form.Get("metricLimit"), request.Form.Get("eventLimit")) if err == nil { - results, err = eventHandler.server.Eventstore.Search(criteria) + results, err = eventHandler.server.Eventstore.Search(ctx, criteria) if err == nil { statusCode = http.StatusOK } else { @@ -69,14 +70,14 @@ func (eventHandler *EventHandler) get(writer http.ResponseWriter, request *http. return statusCode, results, err } -func (eventHandler *EventHandler) ack(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (eventHandler *EventHandler) ack(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { var results *model.EventUpdateResults statusCode := http.StatusBadRequest ackCriteria := model.NewEventAckCriteria() err := json.NewDecoder(request.Body).Decode(&ackCriteria) if err == nil { - results, err = eventHandler.server.Eventstore.Acknowledge(ackCriteria) + results, err = eventHandler.server.Eventstore.Acknowledge(ctx, ackCriteria) if err == nil { statusCode = http.StatusOK } else { diff --git a/server/eventstore.go b/server/eventstore.go index 52d44ce65..ade4dfdf1 100644 --- a/server/eventstore.go +++ b/server/eventstore.go @@ -11,11 +11,12 @@ package server import ( + "context" "github.com/security-onion-solutions/securityonion-soc/model" ) type Eventstore interface { - Search(criteria *model.EventSearchCriteria) (*model.EventSearchResults, error) - Update(criteria *model.EventUpdateCriteria) (*model.EventUpdateResults, error) - Acknowledge(criteria *model.EventAckCriteria) (*model.EventUpdateResults, error) + Search(context context.Context, criteria *model.EventSearchCriteria) (*model.EventSearchResults, error) + Update(context context.Context, criteria *model.EventUpdateCriteria) (*model.EventUpdateResults, error) + Acknowledge(context context.Context, criteria *model.EventAckCriteria) (*model.EventUpdateResults, error) } \ No newline at end of file diff --git a/server/gridhandler.go b/server/gridhandler.go index 9d28e2959..ec9ad71c1 100644 --- a/server/gridhandler.go +++ b/server/gridhandler.go @@ -11,6 +11,7 @@ package server import ( + "context" "errors" "net/http" "github.com/security-onion-solutions/securityonion-soc/web" @@ -29,13 +30,13 @@ func NewGridHandler(srv *Server) *GridHandler { return handler } -func (gridHandler *GridHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (gridHandler *GridHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { switch request.Method { - case http.MethodGet: return gridHandler.get(writer, request) + case http.MethodGet: return gridHandler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } -func (gridHandler *GridHandler) get(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (gridHandler *GridHandler) get(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { return http.StatusOK, gridHandler.server.Datastore.GetNodes(), nil } \ No newline at end of file diff --git a/server/infohandler.go b/server/infohandler.go index 0e1e4945d..fbbdc2e33 100644 --- a/server/infohandler.go +++ b/server/infohandler.go @@ -11,6 +11,7 @@ package server import ( + "context" "errors" "net/http" "os" @@ -31,14 +32,14 @@ func NewInfoHandler(srv *Server) *InfoHandler { return handler } -func (infoHandler *InfoHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (infoHandler *InfoHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { switch request.Method { - case http.MethodGet: return infoHandler.get(writer, request) + case http.MethodGet: return infoHandler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } -func (infoHandler *InfoHandler) get(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +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", diff --git a/server/jobhandler.go b/server/jobhandler.go index 5b9f63833..a981aa913 100644 --- a/server/jobhandler.go +++ b/server/jobhandler.go @@ -11,6 +11,7 @@ package server import ( + "context" "errors" "net/http" "regexp" @@ -32,17 +33,17 @@ func NewJobHandler(srv *Server) *JobHandler { return handler } -func (jobHandler *JobHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (jobHandler *JobHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { switch request.Method { - case http.MethodGet: return jobHandler.get(writer, request) - case http.MethodPost: return jobHandler.post(writer, request) - case http.MethodPut: return jobHandler.put(writer, request) - case http.MethodDelete: return jobHandler.delete(writer, request) + case http.MethodGet: return jobHandler.get(ctx, writer, request) + case http.MethodPost: return jobHandler.post(ctx, writer, request) + case http.MethodPut: return jobHandler.put(ctx, writer, request) + case http.MethodDelete: return jobHandler.delete(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } -func (jobHandler *JobHandler) get(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (jobHandler *JobHandler) get(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { statusCode := http.StatusBadRequest jobId, err := strconv.ParseInt(request.URL.Query().Get("jobId"), 10, 32) job := jobHandler.server.Datastore.GetJob(int(jobId)) @@ -54,22 +55,26 @@ func (jobHandler *JobHandler) get(writer http.ResponseWriter, request *http.Requ return statusCode, job, err } -func (jobHandler *JobHandler) post(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (jobHandler *JobHandler) post(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { statusCode := http.StatusBadRequest job := jobHandler.server.Datastore.CreateJob() err := jobHandler.ReadJson(request, job) if err == nil { - job.UserId = jobHandler.GetUserId(request) - err = jobHandler.server.Datastore.AddJob(job) - if err == nil { - jobHandler.Host.Broadcast("job", job) - statusCode = http.StatusCreated + if user, ok := ctx.Value(web.ContextKeyRequestor).(*model.User); ok { + job.UserId = user.Id + err = jobHandler.server.Datastore.AddJob(job) + if err == nil { + jobHandler.Host.Broadcast("job", job) + statusCode = http.StatusCreated + } + } else { + err = errors.New("User not found in context") } } return statusCode, job, err } -func (jobHandler *JobHandler) put(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (jobHandler *JobHandler) put(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { statusCode := http.StatusBadRequest job := model.NewJob() err := jobHandler.ReadJson(request, job) @@ -87,7 +92,7 @@ func (jobHandler *JobHandler) put(writer http.ResponseWriter, request *http.Requ return statusCode, job, err } -func (jobHandler *JobHandler) delete(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (jobHandler *JobHandler) delete(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { id := jobHandler.GetPathParameter(request.URL.Path, 2) safe, _ := regexp.MatchString(`^[0-9-]+$`, id) if !safe { diff --git a/server/jobshandler.go b/server/jobshandler.go index 9f303dec1..b779b04e0 100644 --- a/server/jobshandler.go +++ b/server/jobshandler.go @@ -11,6 +11,7 @@ package server import ( + "context" "errors" "net/http" "github.com/security-onion-solutions/securityonion-soc/web" @@ -29,13 +30,13 @@ func NewJobsHandler(srv *Server) *JobsHandler { return handler } -func (jobsHandler *JobsHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (jobsHandler *JobsHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { switch request.Method { - case http.MethodGet: return jobsHandler.get(writer, request) + case http.MethodGet: return jobsHandler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } -func (jobsHandler *JobsHandler) get(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (jobsHandler *JobsHandler) get(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { return http.StatusOK, jobsHandler.server.Datastore.GetJobs(), nil } \ No newline at end of file diff --git a/server/modules/elastic/elasticeventstore.go b/server/modules/elastic/elasticeventstore.go index afa0d474a..6170beb12 100644 --- a/server/modules/elastic/elasticeventstore.go +++ b/server/modules/elastic/elasticeventstore.go @@ -13,13 +13,10 @@ package elastic import ( "bytes" "context" - "crypto/tls" "encoding/json" "errors" "fmt" "math" - "net" - "net/http" "strconv" "strings" "sync" @@ -28,6 +25,7 @@ import ( "github.com/elastic/go-elasticsearch/v7" "github.com/elastic/go-elasticsearch/v7/esapi" "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/web" "github.com/tidwall/gjson" ) @@ -116,14 +114,7 @@ func (store *ElasticEventstore) makeEsClient(host string, user string, pass stri Addresses: hosts, Username: user, Password: pass, - Transport: &http.Transport{ - MaxIdleConnsPerHost: 10, - ResponseHeaderTimeout: store.timeoutMs, - DialContext: (&net.Dialer{Timeout: store.timeoutMs}).DialContext, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: !verifyCert, - }, - }, + Transport: NewElasticTransport(user, pass, store.timeoutMs, verifyCert), } maskedPassword := "*****" if len(esConfig.Password) == 0 { @@ -172,14 +163,14 @@ func (store *ElasticEventstore) unmapElasticField(field string) string { return field } -func (store *ElasticEventstore) Search(criteria *model.EventSearchCriteria) (*model.EventSearchResults, error) { - store.refreshCache() +func (store *ElasticEventstore) Search(ctx context.Context, criteria *model.EventSearchCriteria) (*model.EventSearchResults, error) { + store.refreshCache(ctx) results := model.NewEventSearchResults() query, err := convertToElasticRequest(store, criteria) if err == nil { var response string - response, err = store.luceneSearch(query) + response, err = store.luceneSearch(ctx, query) if err == nil { err = convertFromElasticResults(store, response, results) results.Criteria = criteria @@ -200,8 +191,8 @@ func (store *ElasticEventstore) disableCrossClusterIndexing(indexes []string) [] return indexes } -func (store *ElasticEventstore) Update(criteria *model.EventUpdateCriteria) (*model.EventUpdateResults, error) { - store.refreshCache() +func (store *ElasticEventstore) Update(ctx context.Context, criteria *model.EventUpdateCriteria) (*model.EventUpdateResults, error) { + store.refreshCache(ctx) results := model.NewEventUpdateResults() results.Criteria = criteria @@ -211,7 +202,7 @@ func (store *ElasticEventstore) Update(criteria *model.EventUpdateCriteria) (*mo for idx, client := range(store.esAllClients) { log.WithField("clientHost", store.hostUrls[idx]).Debug("Sending request to client") - response, err = store.updateDocuments(client, query, store.disableCrossClusterIndexing(strings.Split(store.index, ",")), !criteria.Asynchronous) + response, err = store.updateDocuments(ctx, client, query, store.disableCrossClusterIndexing(strings.Split(store.index, ",")), !criteria.Asynchronous) if err == nil { if !criteria.Asynchronous { currentResults := model.NewEventUpdateResults() @@ -240,8 +231,8 @@ func (store *ElasticEventstore) Update(criteria *model.EventUpdateCriteria) (*mo return results, err } -func (store *ElasticEventstore) luceneSearch(query string) (string, error) { - return store.indexSearch(query, strings.Split(store.index, ",")) +func (store *ElasticEventstore) luceneSearch(ctx context.Context, query string) (string, error) { + return store.indexSearch(ctx, query, strings.Split(store.index, ",")) } func (store *ElasticEventstore) transformIndex(index string) string { @@ -272,11 +263,14 @@ func (store *ElasticEventstore) readJsonFromResponse(res *esapi.Response) (strin return json, err } -func (store *ElasticEventstore) indexSearch(query string, indexes []string) (string, error) { - log.WithField("query", query).Info("Searching Elasticsearch") +func (store *ElasticEventstore) indexSearch(ctx context.Context, query string, indexes []string) (string, error) { + log.WithFields(log.Fields { + "query": query, + "requestId": ctx.Value(web.ContextKeyRequestId), + }).Info("Searching Elasticsearch") var json string res, err := store.esClient.Search( - store.esClient.Search.WithContext(context.Background()), + store.esClient.Search.WithContext(ctx), store.esClient.Search.WithIndex(indexes...), store.esClient.Search.WithBody(strings.NewReader(query)), store.esClient.Search.WithTrackTotalHits(true), @@ -286,12 +280,18 @@ func (store *ElasticEventstore) indexSearch(query string, indexes []string) (str defer res.Body.Close() json, err = store.readJsonFromResponse(res) } - log.WithFields(log.Fields{"response": json}).Debug("Search finished") + log.WithFields(log.Fields { + "response": json, + "requestId": ctx.Value(web.ContextKeyRequestId), + }).Debug("Search finished") return json, err } -func (store *ElasticEventstore) indexDocument(document string, index string) (string, error) { - log.WithField("document", document).Debug("Adding document to Elasticsearch") +func (store *ElasticEventstore) indexDocument(ctx context.Context, document string, index string) (string, error) { + log.WithFields(log.Fields { + "document": document, + "requestId": ctx.Value(web.ContextKeyRequestId), + }).Debug("Adding document to Elasticsearch") res, err := store.esClient.Index(store.transformIndex(index), strings.NewReader(document), store.esClient.Index.WithRefresh("true")) @@ -302,16 +302,22 @@ func (store *ElasticEventstore) indexDocument(document string, index string) (st defer res.Body.Close() json, err := store.readJsonFromResponse(res) - log.WithFields(log.Fields{"response": json}).Debug("Index new document finished") + log.WithFields(log.Fields{ + "response": json, + "requestId": ctx.Value(web.ContextKeyRequestId), + }).Debug("Index new document finished") return json, err } -func (store *ElasticEventstore) updateDocuments(client *elasticsearch.Client, query string, indexes []string, waitForCompletion bool) (string, error) { - log.WithField("query", query).Debug("Updating documents in Elasticsearch") +func (store *ElasticEventstore) updateDocuments(ctx context.Context, client *elasticsearch.Client, query string, indexes []string, waitForCompletion bool) (string, error) { + log.WithFields(log.Fields { + "query": query, + "requestId": ctx.Value(web.ContextKeyRequestId), + }).Debug("Updating documents in Elasticsearch") var json string res, err := client.UpdateByQuery( indexes, - client.UpdateByQuery.WithContext(context.Background()), + client.UpdateByQuery.WithContext(ctx), client.UpdateByQuery.WithPretty(), client.UpdateByQuery.WithConflicts("proceed"), client.UpdateByQuery.WithBody(strings.NewReader(query)), @@ -322,27 +328,30 @@ func (store *ElasticEventstore) updateDocuments(client *elasticsearch.Client, qu defer res.Body.Close() json, err = store.readJsonFromResponse(res) } - log.WithFields(log.Fields{"response": json}).Debug("Update finished") + log.WithFields(log.Fields{ + "response": json, + "requestId": ctx.Value(web.ContextKeyRequestId), + }).Debug("Update finished") return json, err } -func (store *ElasticEventstore) refreshCache() { +func (store *ElasticEventstore) refreshCache(ctx context.Context) { store.cacheLock.Lock() defer store.cacheLock.Unlock() if store.cacheTime.IsZero() || time.Now().Sub(store.cacheTime) > store.cacheMs { - err := store.refreshCacheFromFieldCaps() + err := store.refreshCacheFromFieldCaps(ctx) if err == nil { store.cacheTime = time.Now() } } } -func (store *ElasticEventstore) refreshCacheFromFieldCaps() error { +func (store *ElasticEventstore) refreshCacheFromFieldCaps(ctx context.Context) error { log.Info("Fetching Field Capabilities from Elasticsearch") indexes := strings.Split(store.index, ",") var json string res, err := store.esClient.FieldCaps( - store.esClient.FieldCaps.WithContext(context.Background()), + store.esClient.FieldCaps.WithContext(ctx), store.esClient.FieldCaps.WithIndex(indexes...), store.esClient.FieldCaps.WithFields("*"), store.esClient.FieldCaps.WithPretty(), @@ -388,12 +397,12 @@ func (store *ElasticEventstore) cacheFields(name gjson.Result, details gjson.Res return true } -func (store *ElasticEventstore) clusterState() (string, error) { +func (store *ElasticEventstore) clusterState(ctx context.Context) (string, error) { log.WithField("cacheMs", store.cacheMs).Debug("Refreshing field definitions") indexes := strings.Split(store.index, ",") var json string res, err := store.esClient.Cluster.State( - store.esClient.Cluster.State.WithContext(context.Background()), + store.esClient.Cluster.State.WithContext(ctx), store.esClient.Cluster.State.WithIndex(indexes...), ) if err == nil { @@ -438,13 +447,14 @@ 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(query string, job *model.Job) error { +func (store *ElasticEventstore) PopulateJobFromDocQuery(ctx context.Context, query string, job *model.Job) error { var outputSensorId string filter := model.NewFilter() - json, err := store.luceneSearch(query) + json, err := store.luceneSearch(ctx, query) log.WithFields(log.Fields{ "query": query, "response": json, + "requestId": ctx.Value(web.ContextKeyRequestId), }).Debug("Elasticsearch primary search finished") if err != nil { log.WithField("query", query).WithError(err).Error("Unable to lookup initial document record") @@ -466,7 +476,7 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(query string, job *model 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) - json, err = store.luceneSearch(query) + json, err = store.luceneSearch(ctx, query) log.WithFields(log.Fields{ "query": query, "response": json, @@ -520,16 +530,18 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(query string, job *model 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) - json, err = store.luceneSearch(query) + json, err = store.luceneSearch(ctx, query) log.WithFields(log.Fields{ "query": query, "response": json, + "requestId": ctx.Value(web.ContextKeyRequestId), }).Debug("Elasticsearch Zeek File search finished") if err != nil { log.WithFields(log.Fields { "query": query, "zeekFileQuery": zeekFileQuery, + "requestId": ctx.Value(web.ContextKeyRequestId), }).WithError(err).Error("Unable to lookup Zeek File record") return err } @@ -539,6 +551,7 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(query string, job *model log.WithFields(log.Fields { "query": query, "zeekFileQuery": zeekFileQuery, + "requestId": ctx.Value(web.ContextKeyRequestId), }).Error("Zeek File record was not found") return errors.New("Unable to locate Zeek File record") } @@ -550,6 +563,7 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(query string, job *model log.WithFields(log.Fields { "query": query, "zeekFileQuery": zeekFileQuery, + "requestId": ctx.Value(web.ContextKeyRequestId), }).Warn("Zeek File record is missing a UID") return errors.New("No valid Zeek connection ID found") } @@ -558,16 +572,18 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(query string, job *model // 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) - json, err = store.luceneSearch(query) + json, err = store.luceneSearch(ctx, query) log.WithFields(log.Fields{ "query": query, "response": json, + "requestId": ctx.Value(web.ContextKeyRequestId), }).Debug("Elasticsearch Zeek search finished") if err != nil { log.WithFields(log.Fields { "query": query, "uid": uid, + "requestId": ctx.Value(web.ContextKeyRequestId), }).WithError(err).Error("Unable to lookup Zeek record") return err } @@ -577,6 +593,7 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(query string, job *model log.WithFields(log.Fields { "query": query, "uid": uid, + "requestId": ctx.Value(web.ContextKeyRequestId), }).Error("Zeek record was not found") return errors.New("Unable to locate Zeek record") } @@ -619,6 +636,7 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(query string, job *model log.WithFields(log.Fields{ "sensorId": outputSensorId, + "requestId": ctx.Value(web.ContextKeyRequestId), }).Info("Obtained output parameters") } @@ -626,6 +644,7 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(query string, job *model log.WithFields(log.Fields { "query": query, "uid": uid, + "requestId": ctx.Value(web.ContextKeyRequestId), }).Warn("Unable to lookup PCAP due to missing TCP/UDP parameters") return errors.New("No TCP/UDP record was found for retrieving PCAP") } @@ -638,7 +657,7 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(query string, job *model return nil } -func (store *ElasticEventstore) Acknowledge(ackCriteria *model.EventAckCriteria) (*model.EventUpdateResults, error) { +func (store *ElasticEventstore) Acknowledge(ctx context.Context, ackCriteria *model.EventAckCriteria) (*model.EventUpdateResults, error) { var results *model.EventUpdateResults var err error if len(ackCriteria.EventFilter) > 0 { @@ -647,6 +666,7 @@ func (store *ElasticEventstore) Acknowledge(ackCriteria *model.EventAckCriteria) "eventFilter": ackCriteria.EventFilter, "escalate": ackCriteria.Escalate, "acknowledge": ackCriteria.Acknowledge, + "requestId": ctx.Value(web.ContextKeyRequestId), }).Info("Acknowledging event") updateCriteria := model.NewEventUpdateCriteria() @@ -679,6 +699,7 @@ func (store *ElasticEventstore) Acknowledge(ackCriteria *model.EventAckCriteria) log.WithFields(log.Fields { key: value, "threshold": store.asyncThreshold, + "requestId": ctx.Value(web.ContextKeyRequestId), }).Info("Acknowledging events asynchronously due to large quantity"); updateCriteria.Asynchronous = true } @@ -688,7 +709,7 @@ func (store *ElasticEventstore) Acknowledge(ackCriteria *model.EventAckCriteria) updateCriteria.ParsedQuery = model.NewQuery() updateCriteria.ParsedQuery.AddSegment(searchSegment) - results, err = store.Update(updateCriteria) + results, err = store.Update(ctx, updateCriteria) if err == nil && !updateCriteria.Asynchronous { if results.UpdatedCount == 0 { if results.UnchangedCount == 0 { diff --git a/server/modules/elastic/elastictransport.go b/server/modules/elastic/elastictransport.go new file mode 100644 index 000000000..cb2a183ca --- /dev/null +++ b/server/modules/elastic/elastictransport.go @@ -0,0 +1,61 @@ +// Copyright 2020-2021 Security Onion Solutions, LLC. All rights reserved. +// +// This program is distributed under the terms of version 2 of the +// GNU General Public License. See LICENSE for further details. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +package elastic + +import ( + "crypto/tls" + "net" + "net/http" + "time" + "github.com/apex/log" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/web" +) + +type ElasticTransport struct { + internal http.RoundTripper +} + +func NewElasticTransport(user string, pass string, timeoutMs time.Duration, verifyCert bool) http.RoundTripper { + httpTransport := &http.Transport{ + MaxIdleConnsPerHost: 10, + ResponseHeaderTimeout: timeoutMs, + DialContext: (&net.Dialer{Timeout: timeoutMs}).DialContext, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: !verifyCert, + }, + } + + if len(user) > 0 && len(pass) > 0 { + return &ElasticTransport { + internal: httpTransport, + } + } + + return httpTransport +} + +func (transport *ElasticTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if user, ok := req.Context().Value(web.ContextKeyRequestor).(*model.User); ok { + log.WithFields(log.Fields { + "username": user.Email, + "searchUsername": user.SearchUsername, + "requestId": req.Context().Value(web.ContextKeyRequestId), + }).Debug("Executing Elastic request on behalf of user") + username := user.Email + if user.SearchUsername != "" { + username = user.SearchUsername + } + req.Header.Set("es-security-runas-user", username) + } else { + log.Warn("User not found in context") + } + return transport.internal.RoundTrip(req) +} diff --git a/server/modules/elastic/elastictransport_test.go b/server/modules/elastic/elastictransport_test.go new file mode 100644 index 000000000..19b78c8b7 --- /dev/null +++ b/server/modules/elastic/elastictransport_test.go @@ -0,0 +1,61 @@ +// Copyright 2019 Jason Ertel (jertel). All rights reserved. +// Copyright 2020-2021 Security Onion Solutions, LLC. All rights reserved. +// +// This program is distributed under the terms of version 2 of the +// GNU General Public License. See LICENSE for further details. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +package elastic + +import ( + "context" + "net/http" + "testing" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/web" +) + +type DummyTransport struct { + username string +} + +func (transport *DummyTransport) RoundTrip(req *http.Request) (*http.Response, error) { + transport.username = req.Header.Get("es-security-runas-user") + return nil, nil +} + +func TestRoundTrip(tester *testing.T) { + dummy := &DummyTransport{} + transport := &ElasticTransport{} + transport.internal = dummy + + user := model.NewUser() + user.Email = "test" + request, _ := http.NewRequest("GET", "", nil) + request = request.WithContext(context.WithValue(context.Background(), web.ContextKeyRequestor, user)) + transport.RoundTrip(request) + + if dummy.username != "test" { + tester.Errorf("Expected username test but got %s", dummy.username) + } +} + +func TestRoundTripSearchUsername(tester *testing.T) { + dummy := &DummyTransport{} + transport := &ElasticTransport{} + transport.internal = dummy + + user := model.NewUser() + user.Email = "test" + user.SearchUsername = "mysearchuser" + request, _ := http.NewRequest("GET", "", nil) + request = request.WithContext(context.WithValue(context.Background(), web.ContextKeyRequestor, user)) + transport.RoundTrip(request) + + if dummy.username != "mysearchuser" { + tester.Errorf("Expected username mysearchuser but got %s", dummy.username) + } +} \ No newline at end of file diff --git a/server/modules/elastic/joblookuphandler.go b/server/modules/elastic/joblookuphandler.go index 09c08f747..4152a2ed1 100644 --- a/server/modules/elastic/joblookuphandler.go +++ b/server/modules/elastic/joblookuphandler.go @@ -11,10 +11,12 @@ package elastic import ( + "context" "errors" "fmt" "net/http" "strconv" + "github.com/security-onion-solutions/securityonion-soc/model" "github.com/security-onion-solutions/securityonion-soc/server" "github.com/security-onion-solutions/securityonion-soc/web" ) @@ -34,14 +36,14 @@ func NewJobLookupHandler(srv *server.Server, store *ElasticEventstore) *JobLooku return handler } -func (handler *JobLookupHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (handler *JobLookupHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { switch request.Method { - case http.MethodGet: return handler.get(writer, request) + case http.MethodGet: return handler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } -func (handler *JobLookupHandler) get(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +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 @@ -53,15 +55,19 @@ func (handler *JobLookupHandler) get(writer http.ResponseWriter, request *http.R } job := handler.server.Datastore.CreateJob() - err := handler.store.PopulateJobFromDocQuery(query, job) + err := handler.store.PopulateJobFromDocQuery(ctx, query, job) if err == nil { - job.UserId = handler.GetUserId(request) - err = handler.server.Datastore.AddJob(job) - if err == nil { - handler.Host.Broadcast("job", job) - statusCode = http.StatusOK - redirectUrl := handler.server.Config.BaseUrl + "#/job/" + strconv.Itoa(job.Id) - http.Redirect(writer, request, redirectUrl, http.StatusFound) + if user, ok := ctx.Value(web.ContextKeyRequestor).(*model.User); ok { + job.UserId = user.Id + err = handler.server.Datastore.AddJob(job) + if err == nil { + handler.Host.Broadcast("job", job) + statusCode = http.StatusOK + redirectUrl := handler.server.Config.BaseUrl + "#/job/" + strconv.Itoa(job.Id) + http.Redirect(writer, request, redirectUrl, http.StatusFound) + } + } else { + err = errors.New("User not found in context") } } else { statusCode = http.StatusNotFound diff --git a/server/modules/kratos/kratos.go b/server/modules/kratos/kratos.go index 6f7e693f0..4bf024046 100644 --- a/server/modules/kratos/kratos.go +++ b/server/modules/kratos/kratos.go @@ -14,6 +14,8 @@ import ( "github.com/security-onion-solutions/securityonion-soc/server" ) +const DEFAULT_CACHE_MS = 60000 + type Kratos struct { config module.ModuleConfig server *server.Server @@ -33,11 +35,13 @@ func (kratos *Kratos) PrerequisiteModules() []string { func (kratos *Kratos) Init(cfg module.ModuleConfig) error { kratos.config = cfg + cacheMs := module.GetIntDefault(cfg, "cacheMs", DEFAULT_CACHE_MS) url, err := module.GetString(cfg, "hostUrl") if err == nil { - err := kratos.impl.Init(url) + err := kratos.impl.Init(url, cacheMs) if err == nil { kratos.server.Userstore = kratos.impl + err = kratos.server.Host.AddPreprocessor(NewKratosPreprocessor(kratos.impl)) } } return err diff --git a/server/modules/kratos/kratosuser.go b/server/modules/kratos/kratosuser.go index 77099066e..f64378271 100644 --- a/server/modules/kratos/kratosuser.go +++ b/server/modules/kratos/kratosuser.go @@ -14,11 +14,12 @@ import ( ) type KratosTraits struct { - Email string `json:"email"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Role string `json:"role"` - Status string `json:"status"` + Email string `json:"email"` + FirstName string `json:"firstName"` + 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 { @@ -33,12 +34,12 @@ func NewTraits(email string, firstName string, lastName string, role string, sta } type KratosAddress struct { - Id string `json:"id"` - Value string `json:"value"` - ExpirationTime time.Time `json:"expires_at"` - VerifiedTime time.Time `json:"verified_at"` - Verified bool `json:"verified"` - VerifiedVia string `json:"via"` + Id string `json:"id"` + Value string `json:"value"` + ExpirationTime time.Time `json:"expires_at"` + VerifiedTime time.Time `json:"verified_at"` + Verified bool `json:"verified"` + VerifiedVia string `json:"via"` } func NewAddress(email string) *KratosAddress { @@ -55,9 +56,9 @@ func NewAddresses(email string) []*KratosAddress { } type KratosUser struct { - Id string `json:"id"` - SchemaId string `json:"schema_id"` - SchemaUrl string `json:"schema_url"` + Id string `json:"id"` + SchemaId string `json:"schema_id"` + SchemaUrl string `json:"schema_url"` Traits *KratosTraits `json:"traits"` Addresses []*KratosAddress `json:"verifiable_addresses"` } @@ -77,6 +78,7 @@ 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) { @@ -88,6 +90,7 @@ 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 5e0423759..cee708111 100644 --- a/server/modules/kratos/kratosuser_test.go +++ b/server/modules/kratos/kratosuser_test.go @@ -21,7 +21,7 @@ func TestCopyFromUser(tester *testing.T) { user.FirstName = "myFirstname" user.LastName = "myLastname" user.Role = "myRole" - user.Role = "locked" + user.Status = "locked" kratosUser.copyFromUser(user) if kratosUser.Traits.Email != user.Email { tester.Errorf("Email failed to convert") @@ -35,8 +35,11 @@ func TestCopyFromUser(tester *testing.T) { if kratosUser.Traits.Role != user.Role { tester.Errorf("Role failed to convert") } - if kratosUser.Traits.Role != user.Role { - tester.Errorf("Role failed to convert") + 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") @@ -45,6 +48,7 @@ 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 { @@ -60,7 +64,10 @@ func TestCopyToUser(tester *testing.T) { tester.Errorf("Role failed to convert") } if kratosUser.Traits.Status != user.Status { - tester.Errorf("Role failed to convert") + 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") diff --git a/server/modules/kratos/kratosuserstore.go b/server/modules/kratos/kratosuserstore.go index 42a150267..fb3717258 100644 --- a/server/modules/kratos/kratosuserstore.go +++ b/server/modules/kratos/kratosuserstore.go @@ -10,13 +10,19 @@ package kratos import ( + "sync" + "time" "github.com/apex/log" "github.com/security-onion-solutions/securityonion-soc/model" "github.com/security-onion-solutions/securityonion-soc/web" ) type KratosUserstore struct { - client *web.Client + client *web.Client + cacheMs time.Duration + cacheLock sync.Mutex + users []*model.User + usersLastUpdated time.Time } func NewKratosUserstore() *KratosUserstore { @@ -24,8 +30,9 @@ func NewKratosUserstore() *KratosUserstore { } } -func (kratos* KratosUserstore) Init(url string) error { +func (kratos* KratosUserstore) Init(url string, cacheMs int) error { kratos.client = web.NewClient(url, true) + kratos.cacheMs = time.Duration(cacheMs) * time.Millisecond return nil } @@ -36,19 +43,26 @@ func (kratos* KratosUserstore) fetchUser(id string) (*KratosUser, error) { } func (kratos *KratosUserstore) GetUsers() ([]*model.User, error) { - kratosUsers := make([]*KratosUser, 0, 0) - _, err := kratos.client.SendObject("GET", "/identities", "", &kratosUsers, false) - if err != nil { - log.WithError(err).Error("Failed to fetch users from Kratos") - return nil, err - } - users := make([]*model.User, 0, 0) - for _, kratosUser := range kratosUsers { - user := model.NewUser() - kratosUser.copyToUser(user) - users = append(users, user) + kratos.cacheLock.Lock() + defer kratos.cacheLock.Unlock() + + if time.Now().Sub(kratos.usersLastUpdated) > kratos.cacheMs { + kratosUsers := make([]*KratosUser, 0, 0) + _, err := kratos.client.SendObject("GET", "/identities", "", &kratosUsers, false) + if err != nil { + log.WithError(err).Error("Failed to fetch users from Kratos") + return nil, err + } + users := make([]*model.User, 0, 0) + for _, kratosUser := range kratosUsers { + user := model.NewUser() + kratosUser.copyToUser(user) + users = append(users, user) + } + kratos.users = users + kratos.usersLastUpdated = time.Now() } - return users, nil + return kratos.users, nil } func (kratos *KratosUserstore) DeleteUser(id string) error { @@ -61,14 +75,19 @@ func (kratos *KratosUserstore) DeleteUser(id string) error { } func (kratos *KratosUserstore) GetUser(id string) (*model.User, error) { - kratosUser, err := kratos.fetchUser(id) - if err != nil { - log.WithError(err).Error("Failed to fetch user from Kratos") - return nil, err + var err error + var user *model.User + + users, err := kratos.GetUsers() + if err == nil { + for _, testUser := range users { + if testUser.Id == id { + user = testUser + break + } + } } - user := model.NewUser() - kratosUser.copyToUser(user) - return user, nil + return user, err } func (kratos *KratosUserstore) UpdateUser(id string, user *model.User) error { diff --git a/server/modules/kratos/kratosuserstore_test.go b/server/modules/kratos/kratosuserstore_test.go index 62b3bcea2..07080a2ed 100644 --- a/server/modules/kratos/kratosuserstore_test.go +++ b/server/modules/kratos/kratosuserstore_test.go @@ -15,7 +15,7 @@ import ( func TestUserstoreInit(tester *testing.T) { ai := NewKratosUserstore() - err := ai.Init("abc") + err := ai.Init("abc", 1) if err != nil { tester.Errorf("unexpected Init error") } diff --git a/server/modules/statickeyauth/statickeyauth.go b/server/modules/statickeyauth/statickeyauth.go index 05045a798..5d1faa8d6 100644 --- a/server/modules/statickeyauth/statickeyauth.go +++ b/server/modules/statickeyauth/statickeyauth.go @@ -41,7 +41,7 @@ func (skmodule *StaticKeyAuth) Init(cfg module.ModuleConfig) error { if err == nil { err = skmodule.impl.Init(key, anonymousCidr) if err == nil { - skmodule.server.Host.Auth = skmodule.impl + err = skmodule.server.Host.AddPreprocessor(skmodule.impl) } } } diff --git a/server/modules/statickeyauth/statickeyauth_test.go b/server/modules/statickeyauth/statickeyauth_test.go index d07561743..0d1898505 100644 --- a/server/modules/statickeyauth/statickeyauth_test.go +++ b/server/modules/statickeyauth/statickeyauth_test.go @@ -35,6 +35,9 @@ func TestAuthInit(tester *testing.T) { } func authInit(tester *testing.T, auth *StaticKeyAuth, cfg module.ModuleConfig, failure bool, expectedCidr string) { + if len(auth.server.Host.Preprocessors()) != 1 { + tester.Errorf("expected one preprocessors to exist prior to init") + } err := auth.Init(cfg) if failure { if err == nil { @@ -46,8 +49,8 @@ func authInit(tester *testing.T, auth *StaticKeyAuth, cfg module.ModuleConfig, f if auth.impl.anonymousNetwork.String() != expectedCidr { tester.Errorf("expected anonymousNetwork %s but got %s", expectedCidr, auth.impl.anonymousNetwork.String()) } - if auth.server.Host.Auth == nil { - tester.Errorf("expected non-nil Host.Auth") + if len(auth.server.Host.Preprocessors()) != 2 { + tester.Errorf("expected two preprocessors to now exist") } } } diff --git a/server/modules/statickeyauth/statickeyauthimpl.go b/server/modules/statickeyauth/statickeyauthimpl.go index 2b997e7dd..3d8ee4425 100644 --- a/server/modules/statickeyauth/statickeyauthimpl.go +++ b/server/modules/statickeyauth/statickeyauthimpl.go @@ -11,11 +11,13 @@ package statickeyauth import ( + "context" + "errors" "net" "net/http" "strings" - "github.com/apex/log" + "github.com/security-onion-solutions/securityonion-soc/web" ) type StaticKeyAuthImpl struct { @@ -24,7 +26,8 @@ type StaticKeyAuthImpl struct { } func NewStaticKeyAuthImpl() *StaticKeyAuthImpl { - return &StaticKeyAuthImpl{} + return &StaticKeyAuthImpl{ + } } func (auth *StaticKeyAuthImpl) Init(apiKey string, anonymousCidr string) error { @@ -34,6 +37,23 @@ func (auth *StaticKeyAuthImpl) Init(apiKey string, anonymousCidr string) error { return err } +func (auth *StaticKeyAuthImpl) PreprocessPriority() int { + return 100 +} + +func (auth *StaticKeyAuthImpl) Preprocess(ctx context.Context, req *http.Request) (context.Context, int, error) { + var statusCode int + var err error + + if !auth.IsAuthorized(req) { + statusCode = http.StatusUnauthorized + err = errors.New("Access denied") + } else { + ctx = context.WithValue(ctx, web.ContextKeyRequestor, "SONODE") + } + return ctx, statusCode, err +} + func (auth *StaticKeyAuthImpl) IsAuthorized(request *http.Request) bool { apiKey := request.Header.Get("Authorization") remoteIp := request.RemoteAddr diff --git a/server/modules/statickeyauth/statickeyauthimpl_test.go b/server/modules/statickeyauth/statickeyauthimpl_test.go index 214539ba9..71b40ab8d 100644 --- a/server/modules/statickeyauth/statickeyauthimpl_test.go +++ b/server/modules/statickeyauth/statickeyauthimpl_test.go @@ -11,7 +11,10 @@ package statickeyauth import ( + "context" + "net/http" "testing" + "github.com/security-onion-solutions/securityonion-soc/web" ) func TestValidateAuthorization(tester *testing.T) { @@ -66,3 +69,36 @@ func TestAuthImplInit(tester *testing.T) { tester.Errorf("expected anonymousNetwork %s but got %s", "1.2.3.4/16", ai.anonymousNetwork.String()) } } + +func TestPreprocessPriority(tester *testing.T) { + handler := NewStaticKeyAuthImpl() + if handler.PreprocessPriority() != 100 { + tester.Error("expected 100 priority") + } +} + +func TestPreprocess(tester *testing.T) { + ai := NewStaticKeyAuthImpl() + err := ai.Init("abc", "1") + ai.apiKey = "123" + request, _ := http.NewRequest("GET", "", nil) + request.Header.Set("authorization", ai.apiKey) + ctx, statusCode, err := ai.Preprocess(context.Background(), request) + if err != nil { + tester.Errorf("Unexpected error: %v", err) + } + if statusCode != 0 { + tester.Errorf("expected 0 statusCode but got %d", statusCode) + } + if ctx == nil { + tester.Errorf("Unexpected nil context return") + } + requestor := ctx.Value(web.ContextKeyRequestor) + if requestor == nil { + tester.Errorf("Expected non-nil requestor") + } + actualId := requestor.(string) + if actualId != "SONODE" { + tester.Errorf("Expected SONODE but got %s", actualId) + } +} \ No newline at end of file diff --git a/server/nodehandler.go b/server/nodehandler.go index 551793302..cd888f2e0 100644 --- a/server/nodehandler.go +++ b/server/nodehandler.go @@ -11,6 +11,7 @@ package server import ( + "context" "errors" "net/http" "github.com/security-onion-solutions/securityonion-soc/model" @@ -30,14 +31,14 @@ func NewNodeHandler(srv *Server) *NodeHandler { return handler } -func (nodeHandler *NodeHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (nodeHandler *NodeHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { switch request.Method { - case http.MethodPost: return nodeHandler.post(writer, request) + case http.MethodPost: return nodeHandler.post(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } -func (nodeHandler *NodeHandler) post(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (nodeHandler *NodeHandler) post(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { var job *model.Job node := model.NewNode("") err := nodeHandler.ReadJson(request, node) diff --git a/server/packethandler.go b/server/packethandler.go index 41b90a001..8e64fc8a5 100644 --- a/server/packethandler.go +++ b/server/packethandler.go @@ -11,6 +11,7 @@ package server import ( + "context" "errors" "net/http" "strconv" @@ -30,14 +31,14 @@ func NewPacketHandler(srv *Server) *PacketHandler { return handler } -func (packetHandler *PacketHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (packetHandler *PacketHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { switch request.Method { - case http.MethodGet: return packetHandler.get(writer, request) + case http.MethodGet: return packetHandler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } -func (packetHandler *PacketHandler) get(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (packetHandler *PacketHandler) get(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { statusCode := http.StatusBadRequest jobId, err := strconv.ParseInt(request.URL.Query().Get("jobId"), 10, 32) if err != nil { diff --git a/server/queryhandler.go b/server/queryhandler.go index 962fa526e..5bed7ec6a 100644 --- a/server/queryhandler.go +++ b/server/queryhandler.go @@ -10,6 +10,7 @@ package server import ( + "context" "errors" "net/http" "regexp" @@ -31,15 +32,15 @@ func NewQueryHandler(srv *Server) *QueryHandler { return handler } -func (queryHandler *QueryHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (queryHandler *QueryHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { switch request.Method { case http.MethodGet: - return queryHandler.get(writer, request) + return queryHandler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } -func (queryHandler *QueryHandler) get(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (queryHandler *QueryHandler) get(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { operation := queryHandler.GetPathParameter(request.URL.Path, 2) safe, _ := regexp.MatchString(`^[a-z]+$`, operation) if !safe { diff --git a/server/streamhandler.go b/server/streamhandler.go index 86fa7484d..3f5801d0a 100644 --- a/server/streamhandler.go +++ b/server/streamhandler.go @@ -11,6 +11,7 @@ package server import ( + "context" "errors" "io" "net/http" @@ -34,15 +35,15 @@ func NewStreamHandler(srv *Server) *StreamHandler { return handler } -func (streamHandler *StreamHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (streamHandler *StreamHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { switch request.Method { - case http.MethodGet: return streamHandler.get(writer, request) - case http.MethodPost: return streamHandler.post(writer, request) + case http.MethodGet: return streamHandler.get(ctx, writer, request) + case http.MethodPost: return streamHandler.post(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } -func (streamHandler *StreamHandler) get(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (streamHandler *StreamHandler) get(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { statusCode := http.StatusBadRequest jobId, err := strconv.ParseInt(request.URL.Query().Get("jobId"), 10, 32) if err != nil { @@ -85,7 +86,7 @@ func (streamHandler *StreamHandler) get(writer http.ResponseWriter, request *htt return statusCode, nil, err } -func (streamHandler *StreamHandler) post(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (streamHandler *StreamHandler) post(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { statusCode := http.StatusBadRequest jobId, err := strconv.ParseInt(request.URL.Query().Get("jobId"), 10, 32) err = streamHandler.server.Datastore.SavePacketStream(int(jobId), request.Body) diff --git a/server/userhandler.go b/server/userhandler.go index a821815d6..6c8656ee8 100644 --- a/server/userhandler.go +++ b/server/userhandler.go @@ -10,6 +10,7 @@ package server import ( + "context" "errors" "net/http" "regexp" @@ -30,18 +31,18 @@ func NewUserHandler(srv *Server) *UserHandler { return handler } -func (userHandler *UserHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (userHandler *UserHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { if userHandler.server.Userstore == nil { return http.StatusMethodNotAllowed, nil, errors.New("Users module not enabled") } switch request.Method { - case http.MethodGet: return userHandler.get(writer, request) + case http.MethodGet: return userHandler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } -func (userHandler *UserHandler) get(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (userHandler *UserHandler) get(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { id := userHandler.GetPathParameter(request.URL.Path, 2) safe, _ := regexp.MatchString(`^[A-Za-z0-9-]+$`, id) if !safe { @@ -54,7 +55,7 @@ func (userHandler *UserHandler) get(writer http.ResponseWriter, request *http.Re return http.StatusOK, user, nil } -func (userHandler *UserHandler) put(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (userHandler *UserHandler) put(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { id := userHandler.GetPathParameter(request.URL.Path, 2) safe, _ := regexp.MatchString(`^[A-Za-z0-9-]+$`, id) if !safe { diff --git a/server/usershandler.go b/server/usershandler.go index 4d79c16af..0c2117bd9 100644 --- a/server/usershandler.go +++ b/server/usershandler.go @@ -10,6 +10,7 @@ package server import ( + "context" "errors" "net/http" "regexp" @@ -29,18 +30,18 @@ func NewUsersHandler(srv *Server) *UsersHandler { return handler } -func (usersHandler *UsersHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (usersHandler *UsersHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { if usersHandler.server.Userstore == nil { return http.StatusMethodNotAllowed, nil, errors.New("Users module not enabled") } switch request.Method { - case http.MethodGet: return usersHandler.get(writer, request) + case http.MethodGet: return usersHandler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } -func (usersHandler *UsersHandler) get(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (usersHandler *UsersHandler) get(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { users, err := usersHandler.server.Userstore.GetUsers() if err != nil { return http.StatusBadRequest, nil, err @@ -48,7 +49,7 @@ func (usersHandler *UsersHandler) get(writer http.ResponseWriter, request *http. return http.StatusOK, users, nil } -func (usersHandler *UsersHandler) delete(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (usersHandler *UsersHandler) delete(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { id := usersHandler.GetPathParameter(request.URL.Path, 2) safe, _ := regexp.MatchString(`^[A-Za-z0-9-]+$`, id) if !safe { diff --git a/web/basehandler.go b/web/basehandler.go index 0b8ab45e4..d9d78a6f8 100644 --- a/web/basehandler.go +++ b/web/basehandler.go @@ -13,8 +13,8 @@ package web import ( "bytes" "compress/gzip" + "context" "encoding/json" - "errors" "net/http" "reflect" "strings" @@ -23,7 +23,7 @@ import ( ) type HandlerImpl interface { - HandleNow(responseWriter http.ResponseWriter, request *http.Request) (int, interface{}, error) + HandleNow(ctx context.Context, responseWriter http.ResponseWriter, request *http.Request) (int, interface{}, error) } type BaseHandler struct { @@ -33,22 +33,19 @@ type BaseHandler struct { func (handler *BaseHandler) Handle(responseWriter http.ResponseWriter, request *http.Request) { var statusCode, contentLength int - var err error + var err error + defer request.Body.Close() start := time.Now() - if handler.Host.Auth == nil { - err = errors.New("Agent auth module has not been initialized; ensure a valid auth module has been defined in the configuration") - } else { - if !handler.Host.Auth.IsAuthorized(request) { - statusCode = http.StatusUnauthorized - responseWriter.WriteHeader(statusCode) - } else { - var obj interface{} - responseWriter.Header().Set("Version", handler.Host.Version) - statusCode, obj, err = handler.Impl.HandleNow(responseWriter, request) - if err == nil && obj != nil { - contentLength, err = handler.WriteJson(responseWriter, request, statusCode, obj) - } + + context, statusCode, err := handler.Host.Preprocess(request.Context(), request) + if err == nil { + var obj interface{} + responseWriter.Header().Set("Version", handler.Host.Version) + + statusCode, obj, err = handler.Impl.HandleNow(context, responseWriter, request.WithContext(context)) + if err == nil && obj != nil { + contentLength, err = handler.WriteJson(responseWriter, request, statusCode, obj) } } stop := time.Now() @@ -56,41 +53,31 @@ func (handler *BaseHandler) Handle(responseWriter http.ResponseWriter, request * if err != nil { log.WithError(err).WithFields(log.Fields{ - "remoteAddr": request.RemoteAddr, - "sourceIp": handler.Host.GetSourceIp(request), - "path": request.URL.Path, - "query": request.URL.Query(), - "impl": reflect.TypeOf(handler.Impl), - "statusCode": statusCode, - "contentLength": contentLength, - "method": request.Method, - "elapsedMs": elapsed, - "userId": handler.GetUserId(request), - }).Error("Failed request") + "requestId": context.Value(ContextKeyRequestId), + "requestor": context.Value(ContextKeyRequestor), + }).Warn("Request did not complete successfully") if statusCode < http.StatusBadRequest { statusCode = http.StatusInternalServerError } responseWriter.WriteHeader(statusCode) - responseWriter.Write([]byte(err.Error())) - } else { - log.WithFields(log.Fields{ - "remoteAddr": request.RemoteAddr, - "sourceIp": handler.Host.GetSourceIp(request), - "path": request.URL.Path, - "query": request.URL.Query(), - "impl": reflect.TypeOf(handler.Impl), - "statusCode": statusCode, - "contentLength": contentLength, - "method": request.Method, - "elapsedMs": elapsed, - "userId": handler.GetUserId(request), - }).Info("Handled request") + bytes := []byte(err.Error()) + contentLength = len(bytes) + responseWriter.Write(bytes) } -} - -func (handler *BaseHandler) GetUserId(request *http.Request) string { - return request.Header.Get("x-user-id") + log.WithFields(log.Fields{ + "remoteAddr": request.RemoteAddr, + "sourceIp": handler.Host.GetSourceIp(request), + "path": request.URL.Path, + "query": request.URL.Query(), + "impl": reflect.TypeOf(handler.Impl), + "statusCode": statusCode, + "contentLength": contentLength, + "method": request.Method, + "elapsedMs": elapsed, + "requestId": context.Value(ContextKeyRequestId), + "requestor": context.Value(ContextKeyRequestor), + }).Info("Handled request") } func (handler *BaseHandler) WriteJson(responseWriter http.ResponseWriter, request *http.Request, statusCode int, obj interface{}) (int, error) { diff --git a/web/basehandler_test.go b/web/basehandler_test.go index 8f0a11a90..663139243 100644 --- a/web/basehandler_test.go +++ b/web/basehandler_test.go @@ -10,7 +10,6 @@ package web import ( - "net/http" "strconv" "testing" ) @@ -54,16 +53,3 @@ func TestGetPathParameter(tester *testing.T) { }) } } - -func TestGetUserId(tester *testing.T) { - handler := NewTestHandler() - request, _ := http.NewRequest("GET", "", nil) - - expectedId := "112233" - request.Header.Set("x-user-id", expectedId) - - actualId := handler.GetUserId(request) - if actualId != expectedId { - tester.Errorf("expected %s but got %s", expectedId, actualId) - } -} diff --git a/web/basepreprocessor.go b/web/basepreprocessor.go new file mode 100644 index 000000000..263aa523e --- /dev/null +++ b/web/basepreprocessor.go @@ -0,0 +1,39 @@ +// Copyright 2019 Jason Ertel (jertel). All rights reserved. +// Copyright 2020-2021 Security Onion Solutions, LLC. All rights reserved. +// +// This program is distributed under the terms of version 2 of the +// GNU General Public License. See LICENSE for further details. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +package web + +import ( + "context" + "net/http" + "github.com/google/uuid" +) + +type ContextKey string +const ContextKeyRequestId = ContextKey("ContextKeyRequestId") +const ContextKeyRequestor = ContextKey("ContextKeyRequestor") + +type BasePreprocessor struct { +} + +func NewBasePreprocessor() *BasePreprocessor { + return &BasePreprocessor { + } +} + +func (Processor *BasePreprocessor) PreprocessPriority() int { + return 0 +} + +func (processor *BasePreprocessor) Preprocess(ctx context.Context, req *http.Request) (context.Context, int, error) { + uuid := uuid.New().String() + ctx = context.WithValue(ctx, ContextKeyRequestId, uuid) + return ctx, 0, nil +} \ No newline at end of file diff --git a/web/basepreprocessor_test.go b/web/basepreprocessor_test.go new file mode 100644 index 000000000..23e8b69b6 --- /dev/null +++ b/web/basepreprocessor_test.go @@ -0,0 +1,39 @@ +// Copyright 2020-2021 Security Onion Solutions. All rights reserved. +// +// This program is distributed under the terms of version 2 of the +// GNU General Public License. See LICENSE for further details. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +package web + +import ( + "context" + "net/http" + "testing" +) + +func TestPreprocessPriority(tester *testing.T) { + handler := NewBasePreprocessor() + if handler.PreprocessPriority() != 0 { + tester.Error("expected 0 priority") + } +} + +func TestPreprocess(tester *testing.T) { + handler := NewBasePreprocessor() + request, _ := http.NewRequest("GET", "", nil) + ctx, statusCode, err := handler.Preprocess(context.Background(), request) + if err != nil { + tester.Error("expected non-nil err") + } + if statusCode != 0 { + tester.Error("expected 0 statusCode") + } + actualId := ctx.Value(ContextKeyRequestId).(string) + if len(actualId) != 36 { + tester.Errorf("Expected a valid UUID but got %s", actualId) + } +} diff --git a/web/host.go b/web/host.go index e6ebc7ca1..49f3d68d4 100644 --- a/web/host.go +++ b/web/host.go @@ -12,7 +12,11 @@ package web import ( "context" + "errors" "net/http" + "reflect" + "sort" + "strconv" "sync" "time" "github.com/apex/log" @@ -23,15 +27,13 @@ type HostHandler interface { Handle(responseWriter http.ResponseWriter, request *http.Request) } -type HostHandlerImpl struct { -} - -type HostAuth interface { - IsAuthorized(request *http.Request) bool +type Preprocessor interface { + PreprocessPriority() int + Preprocess(ctx context.Context, request *http.Request) (context.Context, int, error) } type Host struct { - Auth HostAuth + preprocessors []Preprocessor bindAddress string htmlDir string idleConnectionTimeoutMs int @@ -43,13 +45,19 @@ type Host struct { } func NewHost(address string, htmlDir string, timeoutMs int, version string) *Host { - return &Host { + host := &Host { + preprocessors: make([]Preprocessor, 0), running: false, bindAddress: address, htmlDir: htmlDir, idleConnectionTimeoutMs: timeoutMs, Version: version, } + err := host.AddPreprocessor(NewBasePreprocessor()) + if err != nil { + log.WithError(err).Error("Unable to add base preprocessor") + } + return host } func (host *Host) GetSourceIp(request *http.Request) string { @@ -168,3 +176,68 @@ func (host *Host) manageConnections(sleepDuration time.Duration) { time.Sleep(sleepDuration) } } + +func (host *Host) AddPreprocessor(preprocessor Preprocessor) error { + log.WithFields(log.Fields { + "priority": preprocessor.PreprocessPriority(), + "type": reflect.TypeOf(preprocessor).String(), + }).Info("Adding new preprocessor") + + unsortedMap := make(map[int]Preprocessor) + for _, existing := range host.preprocessors { + unsortedMap[existing.PreprocessPriority()] = existing + } + + if collider, ok := unsortedMap[preprocessor.PreprocessPriority()]; ok { + return errors.New("Preprocessor with priority " + strconv.Itoa(collider.PreprocessPriority()) + " already exists; preprocessors cannot share identical priorities") + } + unsortedMap[preprocessor.PreprocessPriority()] = preprocessor + + sortedList := make([]Preprocessor, 0, len(unsortedMap)) + + priorities := make([]int, 0, len(unsortedMap)) + for priority := range unsortedMap { + priorities = append(priorities, priority) + } + + sort.Ints(priorities) + + for _, priority := range priorities { + preprocessor := unsortedMap[priority] + log.WithFields(log.Fields { + "priority": preprocessor.PreprocessPriority(), + "type": reflect.TypeOf(preprocessor).String(), + }).Debug("Prioritized preprocessor") + sortedList = append(sortedList, preprocessor) + } + + host.preprocessors = sortedList + return nil +} + +/** + * Returns a copy of the list of preprocessors, in their current priority order, + * where the first preprocessor at index 0 is processed first. + */ +func (host *Host) Preprocessors() []Preprocessor { + procs := make([]Preprocessor, len(host.preprocessors)) + copy(procs, host.preprocessors) + return procs +} + +func (host *Host) Preprocess(ctx context.Context, req *http.Request) (context.Context, int, error) { + var statusCode int + var err error + + for _, preprocessor := range host.preprocessors { + log.WithFields(log.Fields { + "priority": preprocessor.PreprocessPriority(), + "type": reflect.TypeOf(preprocessor).String(), + }).Debug("Preprocessing request") + ctx, statusCode, err = preprocessor.Preprocess(ctx, req) + if err != nil { + break + } + } + return ctx, statusCode, err +} \ No newline at end of file diff --git a/web/host_test.go b/web/host_test.go index d19c5c822..3fcd63b0d 100644 --- a/web/host_test.go +++ b/web/host_test.go @@ -11,6 +11,7 @@ package web import ( + "context" "net/http" "testing" "time" @@ -72,4 +73,74 @@ func TestGetSourceIp(tester *testing.T) { if actual != expected { tester.Errorf("expected %s but got %s", expected, actual) } +} + +type DummyPreprocessor struct { + priority int + statusCode int + err error +} + +func (dummy *DummyPreprocessor) PreprocessPriority() int { + return dummy.priority +} + +func (dummy *DummyPreprocessor) Preprocess(ctx context.Context, request *http.Request) (context.Context, int, error) { + return ctx, dummy.statusCode, dummy.err +} + +func TestPreprocessorSetup(tester *testing.T) { + host := NewHost("http://some.where/path", "/tmp/foo", 123, "unit test") + if len(host.Preprocessors()) != 1 { + tester.Errorf("expected one preprocessors on new host") + } + + newPreprocessor := &DummyPreprocessor{ priority: 123, } + + host.AddPreprocessor(newPreprocessor) + if len(host.Preprocessors()) != 2 { + tester.Errorf("expected two preprocessors on host") + } + + if host.Preprocessors()[1] != newPreprocessor { + tester.Errorf("expected new preprocessors to be processed last") + } + + newPreprocessor2 := &DummyPreprocessor{ priority: 12, } + + host.AddPreprocessor(newPreprocessor2) + if len(host.Preprocessors()) != 3 { + tester.Errorf("expected three preprocessors on host") + } + + if host.Preprocessors()[1] != newPreprocessor2 { + tester.Errorf("expected new preprocessors to be processed second") + } + + // Should collide + newPreprocessor3 := &DummyPreprocessor{ priority: 12, } + err := host.AddPreprocessor(newPreprocessor3) + if err == nil { + tester.Errorf("Expected error from colliding preprocessors") + } + if len(host.Preprocessors()) != 3 { + tester.Errorf("expected three preprocessors on host after collision") + } +} + +func TestPreprocessExecute(tester *testing.T) { + host := NewHost("http://some.where/path", "/tmp/foo", 123, "unit test") + newPreprocessor := &DummyPreprocessor{ priority: 123, statusCode: 321} + host.AddPreprocessor(newPreprocessor) + request, _ := http.NewRequest("GET", "", nil) + ctx, statusCode, err := host.Preprocess(context.Background(), request) + if err != nil { + tester.Errorf("Unexpected error during testing preprocessing") + } + if statusCode != 321 { + tester.Errorf("Expected status code 321 but got %d", statusCode) + } + if ctx.Value(ContextKeyRequestId) == nil { + tester.Error("Context mismatch after preprocessing") + } } \ No newline at end of file diff --git a/web/websockethandler.go b/web/websockethandler.go index bb59a9ae9..a920dd4c6 100644 --- a/web/websockethandler.go +++ b/web/websockethandler.go @@ -11,6 +11,7 @@ package web import ( + "context" "errors" "net/http" "github.com/apex/log" @@ -29,7 +30,7 @@ func NewWebSocketHandler(host *Host) *WebSocketHandler { return handler } -func (webSocketHandler *WebSocketHandler) HandleNow(writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { +func (webSocketHandler *WebSocketHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { upgrader := websocket.Upgrader{} connection, err := upgrader.Upgrade(writer, request, nil) ip := webSocketHandler.Host.GetSourceIp(request) From 6bbdb7b000627a0bac53f18f8b53800002633e85 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Wed, 12 May 2021 12:12:57 -0400 Subject: [PATCH 04/29] Refactor for improved Elastic integration --- .gitignore | 1 - server/modules/kratos/kratospreprocessor.go | 47 ++++++++++++++ .../modules/kratos/kratospreprocessor_test.go | 62 +++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 server/modules/kratos/kratospreprocessor.go create mode 100644 server/modules/kratos/kratospreprocessor_test.go diff --git a/.gitignore b/.gitignore index 26ac8f055..7c8ae352d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ version.json config.json sensoroni.log sensoroni -kratos jobs/ nsm/ .vscode/ diff --git a/server/modules/kratos/kratospreprocessor.go b/server/modules/kratos/kratospreprocessor.go new file mode 100644 index 000000000..dfb1c59a5 --- /dev/null +++ b/server/modules/kratos/kratospreprocessor.go @@ -0,0 +1,47 @@ +// Copyright 2019 Jason Ertel (jertel). All rights reserved. +// Copyright 2020-2021 Security Onion Solutions, LLC. All rights reserved. +// +// This program is distributed under the terms of version 2 of the +// GNU General Public License. See LICENSE for further details. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +package kratos + +import ( + "context" + "net/http" + "github.com/security-onion-solutions/securityonion-soc/web" +) + +type KratosPreprocessor struct { + userstore *KratosUserstore +} + +func NewKratosPreprocessor(impl *KratosUserstore) *KratosPreprocessor { + return &KratosPreprocessor{ + userstore: impl, + } +} + +func (proc *KratosPreprocessor) PreprocessPriority() int { + return 110 +} + +func (proc *KratosPreprocessor) Preprocess(ctx context.Context, request *http.Request) (context.Context, int, error) { + var statusCode int + var err error + + userId := request.Header.Get("x-user-id") + if userId != "" { + user, err := proc.userstore.GetUser(userId) + if err == nil { + ctx = context.WithValue(ctx, web.ContextKeyRequestor, user) + } + } + + return ctx, statusCode, err +} + diff --git a/server/modules/kratos/kratospreprocessor_test.go b/server/modules/kratos/kratospreprocessor_test.go new file mode 100644 index 000000000..f5bf60f74 --- /dev/null +++ b/server/modules/kratos/kratospreprocessor_test.go @@ -0,0 +1,62 @@ +// Copyright 2020-2021 Security Onion Solutions, LLC. All rights reserved. +// +// This program is distributed under the terms of version 2 of the +// GNU General Public License. See LICENSE for further details. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +package kratos + +import ( + "context" + "net/http" + "testing" + "time" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/web" +) + +func TestPreprocessPriority(tester *testing.T) { + handler := NewKratosPreprocessor(nil) + if handler.PreprocessPriority() != 110 { + tester.Error("expected 110 priority") + } +} +func TestPreprocess(tester *testing.T) { + expectedId := "112233" + + user := model.NewUser() + user.Id = expectedId + userstore := NewKratosUserstore() + userstore.users = make([]*model.User, 0) + userstore.users = append(userstore.users, user) + userstore.cacheMs = 3600000 + userstore.usersLastUpdated = time.Now() + + handler := NewKratosPreprocessor(userstore) + request, _ := http.NewRequest("GET", "", nil) + + request.Header.Set("x-user-id", expectedId) + + ctx, statusCode, err := handler.Preprocess(context.Background(), request) + if err != nil { + tester.Errorf("Unexpected error: %v", err) + } + if statusCode != 0 { + tester.Errorf("expected 0 statusCode but got %d", statusCode) + } + if ctx == nil { + tester.Errorf("Unexpected nil context return") + } + + requestor := ctx.Value(web.ContextKeyRequestor) + if requestor == nil { + tester.Errorf("Expected non-nil requestor") + } + actualId := requestor.(*model.User).Id + if actualId != expectedId { + tester.Errorf("expected %s but got %s", expectedId, actualId) + } +} From 044a315a4e739bf9debebb7a6c8c70275e253fd8 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Mon, 17 May 2021 18:25:28 -0400 Subject: [PATCH 05/29] Refactor grid interface to support some or all nodes not having metrics enabled --- html/index.html | 15 +- html/js/i18n.js | 1 + html/js/routes/grid.js | 21 +++ html/js/routes/grid.test.js | 26 +++ model/node.go | 12 +- model/node_test.go | 191 ++++++++++----------- server/modules/influxdb/influxdbmetrics.go | 6 +- 7 files changed, 164 insertions(+), 108 deletions(-) diff --git a/html/index.html b/html/index.html index aba0bfb82..dfc7e3144 100644 --- a/html/index.html +++ b/html/index.html @@ -1055,7 +1055,7 @@

{{ i18n.viewJob }}

-

{{ i18n.gridEps }} {{ gridEps | formatCount }}

+

{{ i18n.gridEps }} {{ gridEps | formatCount }}

@@ -1088,7 +1088,10 @@

{{ i18n.gridEps }} {{ gridEps | formatCount }}

- {{ props.item.productionEps | formatCount }} + + {{ props.item.productionEps | formatCount }} + {{ i18n.na }} + {{ props.item.updateTime | formatTimestamp }} {{ props.item.epochTime | formatTimestamp }} {{ props.item.uptimeSeconds | formatDuration }} @@ -1112,15 +1115,15 @@

{{ i18n.gridEps }} {{ gridEps | formatCount }}

{{ i18n.dateOnline }} {{ item.onlineTime | formatDateTime }} -
+
{{ i18n.epsProduction }} {{ item.productionEps | formatCount }}
-
+
{{ i18n.epsConsumption }} {{ item.consumptionEps | formatCount }}
-
+
{{ i18n.nodeStatusProcess }} {{ $root.localizeMessage(item.processStatus) }}
@@ -1128,7 +1131,7 @@

{{ i18n.gridEps }} {{ gridEps | formatCount }}

{{ i18n.nodeStatusConnection }} {{ $root.localizeMessage(item.connectionStatus) }}
-
+
{{ i18n.nodeStatusRaid }} {{ $root.localizeMessage(item.raidStatus) }}
diff --git a/html/js/i18n.js b/html/js/i18n.js index 9926fa599..9b8d5469d 100644 --- a/html/js/i18n.js +++ b/html/js/i18n.js @@ -195,6 +195,7 @@ const i18n = { months: 'months', mruQuery: 'Recently Used', mruQueryHelp: 'This query is a user-defined query and is only available on this browser.', + na: 'N/A', noData: 'No information is currently available.', nodeExpandHelp: 'Show node details', nodeExpand: 'Expand', diff --git a/html/js/routes/grid.js b/html/js/routes/grid.js index 88e7fc413..13410c9e7 100644 --- a/html/js/routes/grid.js +++ b/html/js/routes/grid.js @@ -38,6 +38,7 @@ routes.push({ path: '/grid', name: 'grid', component: { itemsPerPage: 10, footerProps: { 'items-per-page-options': [10,25,50,100,250,1000] }, gridEps: 0, + metricsEnabled: false, }}, created() { Vue.filter('colorNodeStatus', this.colorNodeStatus); @@ -70,6 +71,7 @@ routes.push({ path: '/grid', name: 'grid', component: { this.nodes.forEach(function(node) { route.updateNode(node); }); + this.updateMetricsEnabled(); this.loadLocalSettings(); } catch (error) { this.$root.showError(error); @@ -78,6 +80,21 @@ routes.push({ path: '/grid', name: 'grid', component: { this.$root.subscribe("node", this.updateNode); this.$root.subscribe("status", this.updateStatus); }, + updateMetricsEnabled() { + this.metricsEnabled = !this.nodes.every(function(node) { return !node.metricsEnabled; }); + + const route = this; + const epsColumn = this.headers.find(function(item) { + return item.text == route.i18n.eps + }); + if (epsColumn) { + if (!this.metricsEnabled) { + epsColumn.align = ' d-none'; + } else { + epsColumn.align = ''; + } + } + }, expand(item) { if (this.isExpanded(item)) { this.expanded = []; @@ -101,6 +118,10 @@ routes.push({ path: '/grid', name: 'grid', component: { } }, updateNode(node) { + this.updateNodeDetails(node); + this.updateMetricsEnabled() + }, + updateNodeDetails(node) { var found = false; for (var i = 0; i < this.nodes.length; i++) { if (this.nodes[i].id == node.id) { diff --git a/html/js/routes/grid.test.js b/html/js/routes/grid.test.js index a022147be..3f05a7aca 100644 --- a/html/js/routes/grid.test.js +++ b/html/js/routes/grid.test.js @@ -19,3 +19,29 @@ test('updateStatus', () => { comp.updateStatus(status); expect(comp.gridEps).toBe(12); }); + +test('updateMetricsEnabled', () => { + testUpdateMetricsEnabled(true, false, true); + testUpdateMetricsEnabled(false, false, false); + testUpdateMetricsEnabled(true, true, true); +}); + +function testUpdateMetricsEnabled(node1MetricsEnabled, node2MetricsEnabled, expectedMetricsEnabled) { + const node1 = { metricsEnabled: node1MetricsEnabled }; + const node2 = { metricsEnabled: node2MetricsEnabled }; + comp.nodes = [node1, node2]; + + comp.updateMetricsEnabled(); + + expect(comp.metricsEnabled).toBe(expectedMetricsEnabled); + + const epsColumn = comp.headers.find(function(item) { + return item.text == comp.i18n.eps; + }); + + if (!expectedMetricsEnabled) { + expect(epsColumn.align).toBe(' d-none'); + } else { + expect(epsColumn.align).toBe(''); + } +} diff --git a/model/node.go b/model/node.go index c3b1215ed..8ca3cf0cb 100644 --- a/model/node.go +++ b/model/node.go @@ -38,6 +38,7 @@ type Node struct { ProductionEps int `json:"productionEps"` ConsumptionEps int `json:"consumptionEps"` FailedEvents int `json:"failedEvents"` + MetricsEnabled bool `json:"metricsEnabled"` } func NewNode(id string) *Node { @@ -79,19 +80,22 @@ func (node *Node) updateStatusComponent(currentState string, newState string) st return currentState } -func (node *Node) UpdateOverallStatus() bool { +func (node *Node) UpdateOverallStatus(enhancedStatusEnabled bool) bool { newStatus := NodeStatusUnknown newStatus = node.updateStatusComponent(newStatus, node.ConnectionStatus) - newStatus = node.updateStatusComponent(newStatus, node.RaidStatus) - newStatus = node.updateStatusComponent(newStatus, node.ProcessStatus) + if enhancedStatusEnabled { + newStatus = node.updateStatusComponent(newStatus, node.RaidStatus) + newStatus = node.updateStatusComponent(newStatus, node.ProcessStatus) + } // Special case: If either process or connection status is unknown then show node in error state. - if (node.Role != "so-import" && node.ProcessStatus == NodeStatusUnknown) || + if (enhancedStatusEnabled && node.ProcessStatus == NodeStatusUnknown) || node.ConnectionStatus == NodeStatusUnknown { newStatus = NodeStatusFault } oldStatus := node.Status node.Status = newStatus + node.MetricsEnabled = enhancedStatusEnabled return oldStatus != node.Status } diff --git a/model/node_test.go b/model/node_test.go index 5fcd2bc6b..5feb450d4 100644 --- a/model/node_test.go +++ b/model/node_test.go @@ -43,19 +43,18 @@ func TestSetModel(tester *testing.T) { } func testStatus(tester *testing.T, - role string, + enhancedStatusEnabled bool, nodeStatus string, connectionStatus string, raidStatus string, processStatus string, expectedStatus string) { node := NewNode("") - node.Role = role node.Status = nodeStatus node.ConnectionStatus = connectionStatus node.RaidStatus = raidStatus node.ProcessStatus = processStatus - result := node.UpdateOverallStatus() + result := node.UpdateOverallStatus(enhancedStatusEnabled) shouldChange := nodeStatus != expectedStatus if result != shouldChange { tester.Errorf("Unexpected node status change") @@ -67,115 +66,115 @@ func testStatus(tester *testing.T, func TestUpdateNodeStatusAllUnknown(tester *testing.T) { // If all component statuses are unknown then the node's overall status is fault, regardless of current status. - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) } func TestUpdateNodeStatusOneNotUnknown(tester *testing.T) { // If only one status is not unknown then must be in fault state. - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) - - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) - - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) + + testStatus(tester, true, NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) + + testStatus(tester, true, NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) } func TestUpdateImportNodeStatusOneNotUnknown(tester *testing.T) { // If only one status is not unknown then must be in fault state. - testStatus(tester, "so-import", NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk) - testStatus(tester, "so-import", NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-import", NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-import", NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-import", NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-import", NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) - - testStatus(tester, "so-import", NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk) - testStatus(tester, "so-import", NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-import", NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-import", NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-import", NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-import", NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) - - testStatus(tester, "so-import", NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk) - testStatus(tester, "so-import", NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-import", NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-import", NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-import", NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-import", NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) + testStatus(tester, false, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk) + testStatus(tester, false, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, false, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, false, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, false, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) + testStatus(tester, false, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) + + testStatus(tester, false, NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk) + testStatus(tester, false, NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, false, NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, false, NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, false, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) + testStatus(tester, false, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) + + testStatus(tester, false, NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk) + testStatus(tester, false, NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, false, NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, false, NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, false, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) + testStatus(tester, false, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) } func TestUpdateNodeStatusMultipleNotUnknownOkFirst(tester *testing.T) { // If an earlier component status is Ok then the subsequent status becomes the overall status, regardless of current status. - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusOk, NodeStatusOk, NodeStatusOk, NodeStatusOk) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusOk, NodeStatusOk, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusOk) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault, NodeStatusFault) - - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusOk, NodeStatusOk, NodeStatusOk, NodeStatusOk) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusOk, NodeStatusOk, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusOk) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusFault, NodeStatusFault) - - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusOk, NodeStatusOk, NodeStatusOk, NodeStatusOk) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusOk, NodeStatusOk, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusOk) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusOk, NodeStatusOk, NodeStatusOk, NodeStatusOk) + testStatus(tester, true, NodeStatusUnknown, NodeStatusOk, NodeStatusOk, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusOk) + testStatus(tester, true, NodeStatusUnknown, NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusUnknown, NodeStatusOk, NodeStatusFault, NodeStatusFault) + + testStatus(tester, true, NodeStatusOk, NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusOk, NodeStatusOk, NodeStatusOk, NodeStatusOk) + testStatus(tester, true, NodeStatusOk, NodeStatusOk, NodeStatusOk, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusOk) + testStatus(tester, true, NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusFault, NodeStatusFault) + + testStatus(tester, true, NodeStatusFault, NodeStatusOk, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusOk, NodeStatusOk, NodeStatusOk, NodeStatusOk) + testStatus(tester, true, NodeStatusFault, NodeStatusOk, NodeStatusOk, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusOk, NodeStatusOk) + testStatus(tester, true, NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusFault, NodeStatusFault) } func TestUpdateNodeStatusMultipleNotUnknownFaultFirst(tester *testing.T) { // If an earlier component status is Fault then the subsequent status remains Fault, regardless of current status. - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusFault, NodeStatusOk, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusFault, NodeStatusOk, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusFault, NodeStatusOk, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusFault, NodeStatusOk, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusFault, NodeStatusOk, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusFault, NodeStatusOk, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusFault, NodeStatusOk, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusFault, NodeStatusOk, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusOk, NodeStatusUnknown, NodeStatusFault, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusFault, NodeStatusOk, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusFault, NodeStatusOk, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusOk, NodeStatusFault) - testStatus(tester, "so-standalone", NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusFault, NodeStatusOk, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusFault, NodeStatusOk, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusFault, NodeStatusOk, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusOk, NodeStatusFault) + testStatus(tester, true, NodeStatusFault, NodeStatusUnknown, NodeStatusFault, NodeStatusFault, NodeStatusFault) } \ No newline at end of file diff --git a/server/modules/influxdb/influxdbmetrics.go b/server/modules/influxdb/influxdbmetrics.go index 4cd6b2ab4..389a64d1d 100644 --- a/server/modules/influxdb/influxdbmetrics.go +++ b/server/modules/influxdb/influxdbmetrics.go @@ -111,7 +111,7 @@ func (metrics *InfluxDBMetrics) fetchLatestValuesByHost(measurement string, fiel log.WithError(err).Error("Unable to determine latest value") } } else { - log.Info("Skipping InfluxDB fetch due to disconnected InfluxDB client") + log.Debug("Skipping InfluxDB fetch due to disconnected InfluxDB client") } return values } @@ -249,5 +249,7 @@ func (metrics *InfluxDBMetrics) UpdateNodeMetrics(node *model.Node) bool { node.ProductionEps = metrics.getProductionEps(node.Id) node.ConsumptionEps = metrics.getConsumptionEps(node.Id) node.FailedEvents = metrics.getFailedEvents(node.Id) - return node.UpdateOverallStatus() + + enhancedStatusEnabled := (metrics.client != nil) + return node.UpdateOverallStatus(enhancedStatusEnabled) } \ No newline at end of file From f8178db98c54695e1bb435dac6f6986948ee7e73 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Mon, 24 May 2021 11:19:14 -0400 Subject: [PATCH 06/29] 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 07/29] 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 85fecba19809a5e515ab43ef65818ff4000cff7d Mon Sep 17 00:00:00 2001 From: Doug Burks Date: Tue, 25 May 2021 13:31:48 -0400 Subject: [PATCH 08/29] fix typo --- html/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/index.html b/html/index.html index dfc7e3144..aca0f62ba 100644 --- a/html/index.html +++ b/html/index.html @@ -528,7 +528,7 @@

{{ i18n.events }}

- + fa-search-plus From b4d11c13b3171aab0f9dcf9e405b41032dd0a192 Mon Sep 17 00:00:00 2001 From: doug Date: Wed, 26 May 2021 15:13:33 -0400 Subject: [PATCH 09/29] FEATURE: Pivot from SOC PCAP to CyberChef #1596 --- config/clientparameters.go | 2 + html/index.html | 42 +++++++++-- html/js/app.js | 137 +++++++++++++++++++++++++++++++++ html/js/i18n.js | 1 + html/js/routes/hunt.js | 147 ++---------------------------------- html/js/routes/hunt.test.js | 28 +------ html/js/routes/job.js | 59 +++++++++++++++ 7 files changed, 241 insertions(+), 175 deletions(-) diff --git a/config/clientparameters.go b/config/clientparameters.go index d280e9a23..2c318d46c 100644 --- a/config/clientparameters.go +++ b/config/clientparameters.go @@ -19,6 +19,7 @@ const DEFAULT_MOST_RECENTLY_USED_LIMIT = 5 type ClientParameters struct { HuntingParams HuntingParameters `json:"hunt"` AlertingParams HuntingParameters `json:"alerts"` + JobParams HuntingParameters `json:"job"` DocsUrl string `json:"docsUrl"` CheatsheetUrl string `json:"cheatsheetUrl"` GridParams GridParameters `json:"grid"` @@ -34,6 +35,7 @@ func (config *ClientParameters) Verify() error { var err error err = config.HuntingParams.Verify() err = config.AlertingParams.Verify() + err = config.JobParams.Verify() return err } diff --git a/html/index.html b/html/index.html index aca0f62ba..403073a5a 100644 --- a/html/index.html +++ b/html/index.html @@ -577,7 +577,7 @@

{{ i18n.events }}

{{ i18n.share }} - + fa-copy @@ -585,7 +585,7 @@

{{ i18n.events }}

{{ i18n.copyFieldToClipboard }}
- + fa-copy @@ -593,7 +593,7 @@

{{ i18n.events }}

{{ i18n.copyFieldValueToClipboard }}
- + fa-copy @@ -601,7 +601,7 @@

{{ i18n.events }}

{{ i18n.copyEventToClipboardAsJson }}
- + fa-copy @@ -619,7 +619,7 @@

{{ i18n.events }}

{{ i18n.quickActions }} - + {{ action.icon }} @@ -865,7 +865,7 @@

diff --git a/html/js/app.js b/html/js/app.js index 3d835f6ef..c68fd9ec6 100644 --- a/html/js/app.js +++ b/html/js/app.js @@ -85,6 +85,143 @@ $(document).ready(function() { '$vuetify.theme.dark': 'saveLocalSettings', }, methods: { + formatActionContent(content, event, field, value, uriEncode = true) { + if (!content) return null; + + content = this.replaceActionVar(content, "eventId", event["soc_id"], uriEncode) + content = this.replaceActionVar(content, "field", field, uriEncode) + content = this.replaceActionVar(content, "value", value, uriEncode) + + const fields = this.getDynamicActionFieldNames(content); + const route = this; + if (fields && fields.length > 0) { + fields.forEach(function(field) { + value = event[field]; + content = route.replaceActionVar(content, ":" + field, value, uriEncode) + }); + } + return content; + }, + performAction(event, action) { + if (action && !action.background) return false; + const options = action.options ? action.options : { mode: 'no-cors' }; + options.method = action.method; + if (action.method != 'GET') { + options.body = action.bodyFormatted; + } + const route = this; + fetch(action.linkFormatted, options) + .then(data => { + var link = action.backgroundSuccessLinkFormatted; + if (link) { + if (data.status != null) { + link = route.replaceActionVar(link, "responseCode", data.status, true) + } + if (data.statusText != null) { + link = route.replaceActionVar(link, "responseStatus", data.statusText, true) + } + window.open(link, action.target); + } else { + route.$root.showTip(route.i18n.actionSuccess + route.$root.localizeMessage(action.name)); + } + }) + .catch((error) => { + console.error('Unable to perform background action: ' + error); + var link = action.backgroundFailureLinkFormatted; + if (link) { + link = route.replaceActionVar(link, "error", error.message, true) + window.open(link, action.target); + } else { + route.$root.showTip(route.i18n.actionFailure + route.$root.localizeMessage(action.name)); + } + }); + }, + base64encode(content) { + try { + content = btoa(content); + } catch (e) { + console.error("Failed to base64 encode content: " + e); + } + return content; + }, + escape(content) { + if (content.replace) { + try { + content = content.replace(/\\/g, "\\\\"); + content = content.replace(/\"/g, "\\\""); + } catch (e) { + console.error("Failed to escape content: " + e); + } + } + return content + }, + replaceActionVar(content, field, value, uriEncode) { + if (value === undefined || value == null) return content; + + var encode = function(input) { + if (uriEncode) { + return encodeURIComponent(input); + } + return input; + }; + + content = content.replace("{" + field + "}", encode(value)); + content = content.replace("{" + field + "|base64}", encode(this.base64encode(value))); + content = content.replace("{" + field + "|escape}", encode(this.escape(value))); + content = content.replace("{" + field + "|escape|base64}", encode(this.base64encode(this.escape(value)))); + return content; + }, + copyToClipboard(data, style) { + const buffer = document.getElementById('clipboardBuffer'); + // Convert entire item into text + if (style == 'json') { + data = JSON.stringify(data); + } else if (style == 'kvp') { + var text = ""; + for (const prop in data) { + text += prop + ": " + data[prop] + "\n"; + } + data = text; + } + buffer.value = data; + buffer.select(); + buffer.setSelectionRange(0, 99999); + document.execCommand("copy"); + }, + findEligibleActionLinkForEvent(action, event) { + if (action && action.links) { + for (var idx = 0; idx < action.links.length; idx++) { + const link = action.links[idx]; + + if (this.isActionLinkEligibleForEvent(link, event)) { + return link; + } + } + } + return null; + }, + isActionLinkEligibleForEvent(link, event) { + var eligible = true; + eligible &= (link.indexOf("{eventId}") == -1 || event['soc_id']); + const fields = this.getDynamicActionFieldNames(link); + if (fields && fields.length > 0) { + fields.forEach(function(field) { + value = event[field]; + eligible &= value != undefined && value != null; + }); + } + return eligible; + }, + getDynamicActionFieldNames(url) { + const fields = []; + const matches = url.matchAll(/\{:([a-zA-Z0-9_.]+?)(\|.*?)?\}/g); + for (const match of matches) { + if (match.length > 1) { + fields.push(match[1]); + } + } + return fields; + }, log(msg) { console.log(moment().format() + " | " + msg); }, diff --git a/html/js/i18n.js b/html/js/i18n.js index 9b8d5469d..26948a4c7 100644 --- a/html/js/i18n.js +++ b/html/js/i18n.js @@ -63,6 +63,7 @@ const i18n = { copyEventToClipboardAsKvp: 'Copy full event as field:value pairs', copyFieldToClipboard: 'Copy this value only', copyFieldValueToClipboard: 'Copy as field:value', + copyToClipboard: 'Copy to clipboard', custom: 'Custom', darkMode: 'Dark Mode', dateDataEpoch: 'Earliest PCAP', diff --git a/html/js/routes/hunt.js b/html/js/routes/hunt.js index 6cb1f63f0..8b6eaf159 100644 --- a/html/js/routes/hunt.js +++ b/html/js/routes/hunt.js @@ -647,13 +647,13 @@ const huntComponent = { } } - var link = route.findEligibleActionLinkForEvent(action, event); + var link = route.$root.findEligibleActionLinkForEvent(action, event); if (link) { action.enabled = true; - action.linkFormatted = route.formatActionContent(link, event, field, value, true); - action.bodyFormatted = route.formatActionContent(action.body, event, field, value, action.encodeBody); - action.backgroundSuccessLinkFormatted = route.formatActionContent(action.backgroundSuccessLink, event, field, value, true); - action.backgroundFailureLinkFormatted = route.formatActionContent(action.backgroundFailureLink, event, field, value, true); + action.linkFormatted = route.$root.formatActionContent(link, event, field, value, true); + action.bodyFormatted = route.$root.formatActionContent(action.body, event, field, value, action.encodeBody); + action.backgroundSuccessLinkFormatted = route.$root.formatActionContent(action.backgroundSuccessLink, event, field, value, true); + action.backgroundFailureLinkFormatted = route.$root.formatActionContent(action.backgroundFailureLink, event, field, value, true); } else { action.enabled = false; @@ -669,143 +669,6 @@ const huntComponent = { }); } }, - copyToClipboard(data, style) { - const buffer = document.getElementById('clipboardBuffer'); - // Convert entire item into text - if (style == 'json') { - data = JSON.stringify(data); - } else if (style == 'kvp') { - var text = ""; - for (const prop in data) { - text += prop + ": " + data[prop] + "\n"; - } - data = text; - } - buffer.value = data; - buffer.select(); - buffer.setSelectionRange(0, 99999); - document.execCommand("copy"); - }, - findEligibleActionLinkForEvent(action, event) { - if (action && action.links) { - for (var idx = 0; idx < action.links.length; idx++) { - const link = action.links[idx]; - - if (this.isActionLinkEligibleForEvent(link, event)) { - return link; - } - } - } - return null; - }, - isActionLinkEligibleForEvent(link, event) { - var eligible = true; - eligible &= (link.indexOf("{eventId}") == -1 || event['soc_id']); - const fields = this.getDynamicActionFieldNames(link); - if (fields && fields.length > 0) { - fields.forEach(function(field) { - value = event[field]; - eligible &= value != undefined && value != null; - }); - } - return eligible; - }, - getDynamicActionFieldNames(url) { - const fields = []; - const matches = url.matchAll(/\{:([a-zA-Z0-9_.]+?)(\|.*?)?\}/g); - for (const match of matches) { - if (match.length > 1) { - fields.push(match[1]); - } - } - return fields; - }, - performAction(event, action) { - if (action && !action.background) return false; - const options = action.options ? action.options : { mode: 'no-cors' }; - options.method = action.method; - if (action.method != 'GET') { - options.body = action.bodyFormatted; - } - const route = this; - fetch(action.linkFormatted, options) - .then(data => { - var link = action.backgroundSuccessLinkFormatted; - if (link) { - if (data.status != null) { - link = route.replaceActionVar(link, "responseCode", data.status, true) - } - if (data.statusText != null) { - link = route.replaceActionVar(link, "responseStatus", data.statusText, true) - } - window.open(link, action.target); - } else { - route.$root.showTip(route.i18n.actionSuccess + route.$root.localizeMessage(action.name)); - } - }) - .catch((error) => { - console.error('Unable to perform background action: ' + error); - var link = action.backgroundFailureLinkFormatted; - if (link) { - link = route.replaceActionVar(link, "error", error.message, true) - window.open(link, action.target); - } else { - route.$root.showTip(route.i18n.actionFailure + route.$root.localizeMessage(action.name)); - } - }); - }, - base64encode(content) { - try { - content = btoa(content); - } catch (e) { - console.error("Failed to base64 encode content: " + e); - } - return content; - }, - escape(content) { - if (content.replace) { - try { - content = content.replace(/\\/g, "\\\\"); - content = content.replace(/\"/g, "\\\""); - } catch (e) { - console.error("Failed to escape content: " + e); - } - } - return content - }, - replaceActionVar(content, field, value, uriEncode) { - if (value === undefined || value == null) return content; - - var encode = function(input) { - if (uriEncode) { - return encodeURIComponent(input); - } - return input; - }; - - content = content.replace("{" + field + "}", encode(value)); - content = content.replace("{" + field + "|base64}", encode(this.base64encode(value))); - content = content.replace("{" + field + "|escape}", encode(this.escape(value))); - content = content.replace("{" + field + "|escape|base64}", encode(this.base64encode(this.escape(value)))); - return content; - }, - formatActionContent(content, event, field, value, uriEncode = true) { - if (!content) return null; - - content = this.replaceActionVar(content, "eventId", event["soc_id"], uriEncode) - content = this.replaceActionVar(content, "field", field, uriEncode) - content = this.replaceActionVar(content, "value", value, uriEncode) - - const fields = this.getDynamicActionFieldNames(content); - const route = this; - if (fields && fields.length > 0) { - fields.forEach(function(field) { - value = event[field]; - content = route.replaceActionVar(content, ":" + field, value, uriEncode) - }); - } - return content; - }, filterVisibleFields(eventModule, eventDataset, fields) { if (this.eventFields) { var filteredFields = null; diff --git a/html/js/routes/hunt.test.js b/html/js/routes/hunt.test.js index 0306237b9..f81f38c9f 100644 --- a/html/js/routes/hunt.test.js +++ b/html/js/routes/hunt.test.js @@ -12,32 +12,6 @@ require('./hunt.js'); const comp = getComponent("hunt"); -test('escape', () => { - expect(comp.escape('')).toBe(''); - expect(comp.escape('hello')).toBe('hello'); - expect(comp.escape('hello "bob" the builder\\bricklayer')).toBe('hello \\\"bob\\\" the builder\\\\bricklayer'); - expect(comp.escape(1234)).toBe(1234); -}); - -test('base64encode', () => { - expect(comp.base64encode('')).toBe(''); - expect(comp.base64encode('hello')).toBe('aGVsbG8='); -}); - -test('replaceActionVar', () => { - expect(comp.replaceActionVar('test here', 'foo', 'bar', true)).toBe('test here'); - expect(comp.replaceActionVar('test {bar} here', 'foo', 'bar', true)).toBe('test {bar} here'); - expect(comp.replaceActionVar('test {foo} here', 'foo', 'bar', true)).toBe('test bar here'); - expect(comp.replaceActionVar('test {foo} here', 'foo', 'sand bar', true)).toBe('test sand%20bar here'); - expect(comp.replaceActionVar('test {foo|base64} here', 'foo', 'sand bar', true)).toBe('test c2FuZCBiYXI%3D here'); - expect(comp.replaceActionVar('test {foo|escape} here', 'foo', 'sand "bar\\bad"', false)).toBe('test sand \\\"bar\\\\bad\\\" here'); - expect(comp.replaceActionVar('test {foo|escape} here', 'foo', 'sand "bar\\bad"', true)).toBe('test sand%20%5C%22bar%5C%5Cbad%5C%22 here'); - expect(comp.replaceActionVar('test {foo|escape|base64} here', 'foo', 'sand "bar\\bad"', false)).toBe('test c2FuZCBcImJhclxcYmFkXCI= here'); - expect(comp.replaceActionVar('test {foo|escape|base64} here', 'foo', 'sand "bar\\bad"', true)).toBe('test c2FuZCBcImJhclxcYmFkXCI%3D here'); - expect(comp.replaceActionVar('test {foo} here', 'foo', null, true)).toBe('test {foo} here'); - expect(comp.replaceActionVar('test {foo} here', 'foo', undefined, true)).toBe('test {foo} here'); -}); - test('localizeValue', () => { expect(comp.localizeValue('foo')).toBe('foo'); expect(comp.localizeValue('__missing__')).toBe('*Missing'); @@ -142,4 +116,4 @@ test('sortAlertsAscending', () => { expect(sorted[2]).toBe(item2); expect(sorted[1]).toBe(item3); expect(sorted[0]).toBe(item4); -}); \ No newline at end of file +}); diff --git a/html/js/routes/job.js b/html/js/routes/job.js index 276e3166d..c7babfc44 100644 --- a/html/js/routes/job.js +++ b/html/js/routes/job.js @@ -35,6 +35,13 @@ routes.push({ path: '/job/:jobId', name: 'job', component: { itemsPerPage: 10, footerProps: { 'items-per-page-options': [10,50,250,1000] }, count: 500, + quickActionVisible: false, + quickActionX: 0, + quickActionY: 0, + quickActionEvent: null, + quickActionField: "", + quickActionValue: "", + actions: [], }}, created() { Vue.filter('formatPacketView', this.formatPacketView); @@ -43,6 +50,7 @@ routes.push({ path: '/job/:jobId', name: 'job', component: { }, mounted() { this.loadData(); + this.$root.loadParameters('job', this.initActions); }, destroyed() { this.$root.unsubscribe("job", this.updateJob); @@ -57,6 +65,57 @@ routes.push({ path: '/job/:jobId', name: 'job', component: { 'itemsPerPage': 'saveLocalSettings', }, methods: { + initActions(params) { + this.params = params; + this.actions = params["actions"]; + }, + jobClickHandler() { + if (window.getSelection().toString() != '') { + this.toggleQuickAction(event, {}, 'userSelection', window.getSelection().toString()); + window.getSelection().empty(); + } + }, + toggleQuickAction(domEvent, event, field, value) { + if (!domEvent || this.quickActionVisible) { + this.quickActionVisible = false; + return; + } + + if (value) { + var route = this; + this.actions.forEach(function(action, index) { + if (action.fields) { + action.enabled = false; + for (var x = 0; x < action.fields.length; x++) { + if (action.fields[x] == field) { + action.enabled = true; + break; + } + } + } + + var link = route.$root.findEligibleActionLinkForEvent(action, event); + if (link) { + action.enabled = true; + action.linkFormatted = route.$root.formatActionContent(link, event, field, value, true); + action.bodyFormatted = route.$root.formatActionContent(action.body, event, field, value, action.encodeBody); + action.backgroundSuccessLinkFormatted = route.$root.formatActionContent(action.backgroundSuccessLink, event, field, value, true); + action.backgroundFailureLinkFormatted = route.$root.formatActionContent(action.backgroundFailureLink, event, field, value, true); + + } else { + action.enabled = false; + } + }); + this.quickActionEvent = event; + this.quickActionField = field; + this.quickActionValue = value; + this.quickActionX = domEvent.clientX; + this.quickActionY = domEvent.clientY; + this.$nextTick(() => { + this.quickActionVisible = true; + }); + } + }, getPacketColumnSpan() { return this.isOptionEnabled('packets') ? this.headers.length : 1; }, From 26754b607f44acc0d978bc346c92fd3054761733 Mon Sep 17 00:00:00 2001 From: doug Date: Wed, 26 May 2021 16:29:41 -0400 Subject: [PATCH 10/29] fix duplicate hunt-quick-action entries --- html/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/html/index.html b/html/index.html index 403073a5a..ab4b9e334 100644 --- a/html/index.html +++ b/html/index.html @@ -1046,7 +1046,7 @@

{{ i18n.viewJob }}

- + fa-copy @@ -1056,7 +1056,7 @@

{{ i18n.viewJob }}

- + - Click to drill down into these events + {{ i18n.quickDrilldown }}
diff --git a/html/js/i18n.js b/html/js/i18n.js index 26948a4c7..085b5d798 100644 --- a/html/js/i18n.js +++ b/html/js/i18n.js @@ -222,6 +222,7 @@ const i18n = { pcap: 'PCAP', pending: 'Pending', product: 'Security Onion', + quickDrilldown: 'Quick Drilldown', queriesHelp: 'Choose from several pre-defined queries', queryHelp: 'Specify a hunting query in Onion Query Language (OQL)', quickActions: 'Actions', diff --git a/html/js/routes/hunt.js b/html/js/routes/hunt.js index 66e40828c..087a8d1d9 100644 --- a/html/js/routes/hunt.js +++ b/html/js/routes/hunt.js @@ -624,8 +624,8 @@ const huntComponent = { return route; }, countDrilldown(event) { - if ( this.category == 'alerts' && Object.keys(event).length == 4 && Object.keys(event)[0] == "count" && Object.keys(event)[1] == "rule.name" && Object.keys(event)[2] == "event.module" && Object.keys(event)[3] == "event.severity_label" ) { - this.filterRouteDrilldown = this.buildFilterRoute('rule.name', event['rule.name'], FILTER_DRILLDOWN); + if ( (Object.keys(event).length == 2 && Object.keys(event)[0] == "count") || (Object.keys(event).length == 4 && Object.keys(event)[0] == "count" && Object.keys(event)[1] == "rule.name" && Object.keys(event)[2] == "event.module" && Object.keys(event)[3] == "event.severity_label") ) { + this.filterRouteDrilldown = this.buildFilterRoute(Object.keys(event)[1], event[Object.keys(event)[1]], FILTER_DRILLDOWN); this.$router.push(this.filterRouteDrilldown); } }, From 9f7348cc698c667dee39ef073ed95e617efe845e Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Wed, 9 Jun 2021 16:39:39 -0400 Subject: [PATCH 17/29] 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 effb1f2df8dcb6c695ec1f5888e01ae25d314e00 Mon Sep 17 00:00:00 2001 From: Doug Burks Date: Wed, 9 Jun 2021 17:02:10 -0400 Subject: [PATCH 18/29] Fix ordering --- html/js/i18n.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/js/i18n.js b/html/js/i18n.js index 085b5d798..f531ae821 100644 --- a/html/js/i18n.js +++ b/html/js/i18n.js @@ -222,10 +222,10 @@ const i18n = { pcap: 'PCAP', pending: 'Pending', product: 'Security Onion', - quickDrilldown: 'Quick Drilldown', queriesHelp: 'Choose from several pre-defined queries', queryHelp: 'Specify a hunting query in Onion Query Language (OQL)', quickActions: 'Actions', + quickDrilldown: 'Quick Drilldown', reason: 'Reason', reconnecting: 'Attempting to connect to manager', refresh: 'Refresh', From 808c3930e61d1b233a84a087e53cb4ecf834c418 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Thu, 10 Jun 2021 09:15:01 -0400 Subject: [PATCH 19/29] Add log warnings when SOC can't find host metrics from influxdb --- server/modules/influxdb/influxdbmetrics.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/modules/influxdb/influxdbmetrics.go b/server/modules/influxdb/influxdbmetrics.go index 389a64d1d..8d7047dd1 100644 --- a/server/modules/influxdb/influxdbmetrics.go +++ b/server/modules/influxdb/influxdbmetrics.go @@ -194,6 +194,11 @@ func (metrics *InfluxDBMetrics) getRaidStatus(host string) string { case 0: status = model.NodeStatusOk case 1: status = model.NodeStatusFault } + } else { + log.WithFields(log.Fields { + "host": host, + "raidStatus": metrics.raidStatus, + }).Warn("Host not found in raid status metrics") } return status @@ -209,6 +214,11 @@ func (metrics *InfluxDBMetrics) getProcessStatus(host string) string { case 0: status = model.NodeStatusOk case 1: status = model.NodeStatusFault } + } else { + log.WithFields(log.Fields { + "host": host, + "processStatus": metrics.processStatus, + }).Warn("Host not found in process status metrics") } return status From d6f883d35c4c5d4cf0ffc7914d98d318ef0feae4 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Thu, 10 Jun 2021 15:20:33 -0400 Subject: [PATCH 20/29] 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 21/29] 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 22/29] 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 23/29] 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}} Date: Fri, 11 Jun 2021 16:47:10 -0400 Subject: [PATCH 24/29] FIX: Change Acknowledge and Escalate buttons from title to tooltip Security-Onion-Solutions/securityonion#4497 --- html/index.html | 72 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/html/index.html b/html/index.html index 5c001d6dc..0edf93674 100644 --- a/html/index.html +++ b/html/index.html @@ -425,18 +425,30 @@

{{ i18n.groups }}