From fa1b1e9d94837fc7e2997845548adc1163e46b67 Mon Sep 17 00:00:00 2001 From: William Wernert Date: Tue, 10 Aug 2021 15:02:14 -0400 Subject: [PATCH 01/32] Switch to jsdom This will keep us from needing to mock/replace dom objects/functions --- html/js/test_common.js | 14 -------------- jest.config.js | 2 +- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/html/js/test_common.js b/html/js/test_common.js index 74c7b0c9..da144584 100644 --- a/html/js/test_common.js +++ b/html/js/test_common.js @@ -8,20 +8,6 @@ // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -//////////////////////////////////// -// Mock browser -//////////////////////////////////// -global.document = {}; -global.navigator = {}; -global.location = {}; -global.localStorage = {}; -global.btoa = function(content) { - return Buffer.from(content, 'binary').toString('base64'); -}; -global.atob = function(content) { - return Buffer.from(content, 'base64').toString('binary') -}; - //////////////////////////////////// // Mock jQuery //////////////////////////////////// diff --git a/jest.config.js b/jest.config.js index def3e80f..e8b73fe0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -137,7 +137,7 @@ module.exports = { // snapshotSerializers: [], // The test environment that will be used for testing - testEnvironment: "node", + testEnvironment: "jsdom", // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, From 996c861a31be1b1eee9efaa0d75af9ea3ff086c2 Mon Sep 17 00:00:00 2001 From: William Wernert Date: Tue, 10 Aug 2021 15:03:17 -0400 Subject: [PATCH 02/32] Set window.open() target arg to '_self' when testing flag found --- html/js/app.js | 1 + html/js/routes/job.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/html/js/app.js b/html/js/app.js index a8dcd1df..40ab5880 100644 --- a/html/js/app.js +++ b/html/js/app.js @@ -111,6 +111,7 @@ $(document).ready(function() { if (action.method != 'GET') { options.body = action.bodyFormatted; } + action.target = localStorage['settings.flags.testing'] !== 'true' ? action.target : '_self'; const route = this; fetch(action.linkFormatted, options) .then(data => { diff --git a/html/js/routes/job.js b/html/js/routes/job.js index d5b1fa17..15cb4328 100644 --- a/html/js/routes/job.js +++ b/html/js/routes/job.js @@ -189,8 +189,10 @@ routes.push({ path: '/job/:jobId', name: 'job', component: { }, transcriptCyberChef() { const transcript = this.packetArrayTranscript(); + let openArgs = ['/cyberchef/#recipe=From_Hexdump()']; + if (localStorage['settings.flags.testing'] === 'true') { openArgs.push('_self'); }; - const win = window.open("/cyberchef/#recipe=From_Hexdump()"); + const win = window.open(...openArgs); win.onload = () => { win.app.setInput(transcript); }; }, toggleWrap() { From 7cddd96b253239a0d0bbc7f10808fa7148cdab06 Mon Sep 17 00:00:00 2001 From: William Wernert Date: Tue, 10 Aug 2021 15:03:40 -0400 Subject: [PATCH 03/32] Fix formatting in job.test.js --- html/js/routes/job.test.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/html/js/routes/job.test.js b/html/js/routes/job.test.js index 7030583b..3acc768e 100644 --- a/html/js/routes/job.test.js +++ b/html/js/routes/job.test.js @@ -1,28 +1,27 @@ require('../test_common.js') require('./job.js') -const comp = getComponent('job') // What component do I want to get? +const comp = getComponent('job'); test('packetArrayTranscript', () => { - + // Setup const packetArr = [ { }, // no payload, should be filtered { payload: 'SUdOT1JFLlRISVMuVGVzdC5TdHJpbmcuMTIzLmFzZGZhc2RmLmFzZGZhc2Q=', payloadOffset: 0 }, // payloadOffset == 0, should be filtered { payload: 'SUdOT1JFLlRISVMuVGVzdC5TdHJpbmcuMTIzLmFzZGZhc2RmLmFzZGZhc2Q=', payloadOffset: 12 }, { payload: 'SUdOT1JFLlRISVMuVGhpcy5pcy5hLnNlY29uZC50ZXN0LnBhY2tldC4xMjM=', payloadOffset: 12 } - - ] - comp.packets = packetArr + ]; + comp.packets = packetArr; expectedTranscript = `\ 0000 54 65 73 74 2E 53 74 72 69 6E 67 2E 31 32 33 2E Test.String.123. 0016 61 73 64 66 61 73 64 66 2E 61 73 64 66 61 73 64 asdfasdf.asdfasd 0000 54 68 69 73 2E 69 73 2E 61 2E 73 65 63 6F 6E 64 This.is.a.second 0016 2E 74 65 73 74 2E 70 61 63 6B 65 74 2E 31 32 33 .test.packet.123 -` - - const transcript = comp.packetArrayTranscript() +`; - expect(transcript).toBe(expectedTranscript) + // Test + const transcript = comp.packetArrayTranscript(); + expect(transcript).toBe(expectedTranscript); }); From 154b1d85a2f0365ecfd9ad15f490fec2d20ead17 Mon Sep 17 00:00:00 2001 From: William Wernert Date: Tue, 10 Aug 2021 15:04:01 -0400 Subject: [PATCH 04/32] Add test around window.open() conditional arg --- html/js/routes/job.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/html/js/routes/job.test.js b/html/js/routes/job.test.js index 3acc768e..97ebe46a 100644 --- a/html/js/routes/job.test.js +++ b/html/js/routes/job.test.js @@ -25,3 +25,19 @@ test('packetArrayTranscript', () => { expect(transcript).toBe(expectedTranscript); }); +test('transcriptCyberChef_testing', () => { + // Setup + const path = '/cyberchef/#recipe=From_Hexdump()'; + localStorage['settings.flags.testing'] = 'true'; + const mockedOpen = jest.fn(); + mockedOpen.mockReturnValue({}); + const originalOpen = window.open; + window.open = mockedOpen; + + // Test + comp.transcriptCyberChef(); + expect(mockedOpen).toBeCalledWith(path, '_self'); + + // Cleanup + window.open = originalOpen; +}); From 1e41b992277a04491fcac7b601957bd5e686469c Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Thu, 2 Sep 2021 09:38:32 -0400 Subject: [PATCH 05/32] Auth enhancements --- fake/server.go | 62 +++++ go.mod | 1 + go.sum | 2 + html/index.html | 1 + html/js/app.js | 6 + html/js/app.test.js | 12 + html/js/routes/users.js | 1 + model/event_test.go | 67 ++--- model/unauthorized.go | 35 +++ model/unauthorized_test.go | 24 ++ model/user.go | 28 +-- rbac/permissions | 16 ++ rbac/roles | 7 + server/modules/influxdb/influxdbmetrics.go | 78 +++--- server/modules/kratos/kratos.go | 15 +- server/modules/kratos/kratospreprocessor.go | 3 +- .../modules/kratos/kratospreprocessor_test.go | 57 +++-- server/modules/kratos/kratosuser.go | 56 ++--- server/modules/kratos/kratosuser_test.go | 49 ++-- server/modules/kratos/kratosuserstore.go | 83 ++++--- server/modules/kratos/kratosuserstore_test.go | 29 ++- server/modules/modules.go | 4 +- server/modules/modules_test.go | 2 +- .../modules/staticrbac/rbac_permissions.test | 5 + server/modules/staticrbac/rbac_roles.test | 9 + server/modules/staticrbac/staticrbac.go | 63 +++++ server/modules/staticrbac/staticrbac_test.go | 34 +++ .../staticrbac/staticrbacauthorizer.go | 235 ++++++++++++++++++ .../staticrbac/staticrbacauthorizer_test.go | 144 +++++++++++ server/roleshandler.go | 50 ++++ server/rolestore.go | 21 ++ server/server.go | 25 +- server/userhandler.go | 2 +- server/usershandler.go | 2 +- server/userstore.go | 5 +- web/basehandler.go | 7 +- web/basehandler_test.go | 43 ++-- web/basepreprocessor.go | 1 + web/basepreprocessor_test.go | 18 +- web/client.go | 54 ++-- web/client_test.go | 40 +-- web/connection_test.go | 18 +- web/host_test.go | 127 ++++------ 43 files changed, 1149 insertions(+), 392 deletions(-) create mode 100644 fake/server.go create mode 100644 model/unauthorized.go create mode 100644 model/unauthorized_test.go create mode 100644 rbac/permissions create mode 100644 rbac/roles create mode 100644 server/modules/staticrbac/rbac_permissions.test create mode 100644 server/modules/staticrbac/rbac_roles.test create mode 100644 server/modules/staticrbac/staticrbac.go create mode 100644 server/modules/staticrbac/staticrbac_test.go create mode 100644 server/modules/staticrbac/staticrbacauthorizer.go create mode 100644 server/modules/staticrbac/staticrbacauthorizer_test.go create mode 100644 server/roleshandler.go create mode 100644 server/rolestore.go diff --git a/fake/server.go b/fake/server.go new file mode 100644 index 00000000..96f92813 --- /dev/null +++ b/fake/server.go @@ -0,0 +1,62 @@ +// 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 fake + +import ( + "context" + "github.com/security-onion-solutions/securityonion-soc/config" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/server" +) + +type FakeAuthorizer struct { + authorized bool +} + +func (fake FakeAuthorizer) CheckContextOperationAuthorized(ctx context.Context, operation string, target string) error { + if fake.authorized { + return nil + } + return model.NewUnauthorized("fake-subject", operation, target) +} + +type FakeRolestore struct { + roleMap map[string][]string +} + +func (impl *FakeRolestore) GetAssignments(ctx context.Context) (map[string][]string, error) { + return impl.roleMap, nil +} + +func (impl *FakeRolestore) PopulateUserRoles(ctx context.Context, user *model.User) error { + user.Roles = impl.roleMap[user.Email] + return nil +} + +func NewFakeServer(authorized bool, roleMap map[string][]string) *server.Server { + cfg := &config.ServerConfig{} + srv := server.NewServer(cfg, "") + srv.Authorizer = &FakeAuthorizer{ + authorized: authorized, + } + srv.Rolestore = &FakeRolestore{ + roleMap: roleMap, + } + return srv +} + +func NewAuthorizedServer(roleMap map[string][]string) *server.Server { + return NewFakeServer(true, roleMap) +} + +func NewUnauthorizedServer() *server.Server { + return NewFakeServer(false, make(map[string][]string)) +} diff --git a/go.mod b/go.mod index 4c9365aa..474211a0 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,6 @@ require ( 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 + github.com/stretchr/testify v1.7.0 // indirect github.com/tidwall/gjson v1.6.8 ) diff --git a/go.sum b/go.sum index 9822cd3e..52560031 100644 --- a/go.sum +++ b/go.sum @@ -109,6 +109,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tidwall/gjson v1.6.8 h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w= github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= diff --git a/html/index.html b/html/index.html index 27a61beb..e28230dc 100644 --- a/html/index.html +++ b/html/index.html @@ -804,6 +804,7 @@

+ fa-check fa-lock diff --git a/html/js/app.js b/html/js/app.js index 40ab5880..fe61535b 100644 --- a/html/js/app.js +++ b/html/js/app.js @@ -412,6 +412,12 @@ $(document).ready(function() { formatCount(count) { return Number(count).toLocaleString(); }, + formatStringArray(strArray) { + if (strArray != null && strArray.length > 0) { + return strArray.join(", "); + } + return ""; + }, localizeMessage(origMsg) { var msg = origMsg; if (msg.response && msg.response.data) { diff --git a/html/js/app.test.js b/html/js/app.test.js index 5a58f7a6..b4acfae2 100644 --- a/html/js/app.test.js +++ b/html/js/app.test.js @@ -37,3 +37,15 @@ test('replaceActionVar', () => { expect(app.replaceActionVar('test {foo} here', 'foo', null, true)).toBe('test {foo} here'); expect(app.replaceActionVar('test {foo} here', 'foo', undefined, true)).toBe('test {foo} here'); }); + +test('base64encode', () => { + expect(app.base64encode('')).toBe(''); + expect(app.base64encode('hello')).toBe('aGVsbG8='); +}); + +test('formatStringArray', () => { + expect(app.formatStringArray(['hi','there','foo'])).toBe('hi, there, foo'); + expect(app.formatStringArray(['hi','there'])).toBe('hi, there'); + expect(app.formatStringArray(['hi'])).toBe('hi'); + expect(app.formatStringArray([])).toBe(''); +}); \ No newline at end of file diff --git a/html/js/routes/users.js b/html/js/routes/users.js index ac82f49d..7b3eae3e 100644 --- a/html/js/routes/users.js +++ b/html/js/routes/users.js @@ -16,6 +16,7 @@ routes.push({ path: '/users', name: 'users', component: { { text: this.$root.i18n.email, value: 'email' }, { text: this.$root.i18n.firstName, value: 'firstName' }, { text: this.$root.i18n.lastName, value: 'lastName' }, + { text: this.$root.i18n.role, value: 'role' }, { text: this.$root.i18n.status, value: 'status' }, { text: this.$root.i18n.actions }, ], diff --git a/model/event_test.go b/model/event_test.go index 41713d76..a6437c69 100644 --- a/model/event_test.go +++ b/model/event_test.go @@ -11,22 +11,19 @@ package model import ( + "github.com/stretchr/testify/assert" "testing" "time" ) func TestNewEventSearchCriteria(tester *testing.T) { event := NewEventSearchCriteria() - if event.CreateTime.IsZero() { - tester.Errorf("expected CreateTime to be auto populated") - } + assert.NotZero(tester, event.CreateTime) } func TestNewEventUpdateCriteria(tester *testing.T) { event := NewEventUpdateCriteria() - if event.CreateTime.IsZero() { - tester.Errorf("expected CreateTime to be auto populated") - } + assert.NotZero(tester, event.CreateTime) } func TestPopulateQueryTrim(tester *testing.T) { @@ -34,9 +31,7 @@ func TestPopulateQueryTrim(tester *testing.T) { zone := "America/New_York" criteria := NewEventSearchCriteria() _ = criteria.Populate(" foo ", goodTime, time.RFC3339, zone, "10", "100") - if criteria.RawQuery != "foo" { - tester.Errorf("Expected empty query string") - } + assert.Equal(tester, criteria.RawQuery, "foo") } func TestPopulateBadInputTimes(tester *testing.T) { @@ -44,57 +39,39 @@ func TestPopulateBadInputTimes(tester *testing.T) { goodTime := "2006-05-07T14:15:59+01:00" zone := "America/New_York" criteria := NewEventSearchCriteria() - err := criteria.Populate("foo", badTime + " - " + badTime, time.RFC3339, zone, "10", "100") - if err == nil { - tester.Errorf("expected error from bad time input") - } - err = criteria.Populate("foo", badTime + " - " + goodTime, time.RFC3339, zone, "10", "100") - if err == nil { - tester.Errorf("expected error from bad begin time input") - } - err = criteria.Populate("foo", goodTime + " - " + badTime, time.RFC3339, zone, "10", "100") - if err == nil { - tester.Errorf("expected error from bad end time input") - } - err = criteria.Populate("foo", goodTime + " - " + goodTime, time.RFC3339, zone, "30", "100") - if err != nil { - tester.Errorf("expected no error from good time input: %v", err) - } + err := criteria.Populate("foo", badTime+" - "+badTime, time.RFC3339, zone, "10", "100") + assert.Error(tester, err, "expected error from bad start time and end time input") + + err = criteria.Populate("foo", badTime+" - "+goodTime, time.RFC3339, zone, "10", "100") + assert.Error(tester, err, "expected error from bad start time input") + + err = criteria.Populate("foo", goodTime+" - "+badTime, time.RFC3339, zone, "10", "100") + assert.Error(tester, err, "expected error from bad end time input") + + err = criteria.Populate("foo", goodTime+" - "+goodTime, time.RFC3339, zone, "30", "100") + assert.NoError(tester, err, "expected no error from good time input") } func TestLimits(tester *testing.T) { goodTime := "2006-05-07T14:15:59+01:00" criteria := NewEventSearchCriteria() - _ = criteria.Populate("foo", goodTime + " - " + goodTime, time.RFC3339, "PST", "30", "100") - if criteria.EventLimit != 100 { - tester.Errorf("Incorrect event limit: %d", criteria.EventLimit) - } - if criteria.MetricLimit != 30 { - tester.Errorf("Incorrect event limit: %d", criteria.MetricLimit) - } + _ = criteria.Populate("foo", goodTime+" - "+goodTime, time.RFC3339, "PST", "30", "100") + assert.Equal(tester, criteria.EventLimit, 100) + assert.Equal(tester, criteria.MetricLimit, 30) } func TestNewEventSearchResult(tester *testing.T) { event := NewEventSearchResults() time.Sleep(1) event.Complete() - if !event.CompleteTime.After(event.CreateTime) { - tester.Errorf("expected CompleteTime to be newer than CreateTime") - } - if len(event.Errors) != 0 { - tester.Errorf("expected no errors") - } + assert.True(tester, event.CompleteTime.After(event.CreateTime), "expected CompleteTime to be newer than CreateTime") + assert.Len(tester, event.Errors, 0) } - func TestNewEventUpdateResult(tester *testing.T) { event := NewEventUpdateResults() time.Sleep(1) event.Complete() - if !event.CompleteTime.After(event.CreateTime) { - tester.Errorf("expected CompleteTime to be newer than CreateTime") - } - if len(event.Errors) != 0 { - tester.Errorf("expected no errors") - } + assert.True(tester, event.CompleteTime.After(event.CreateTime), "expected CompleteTime to be newer than CreateTime") + assert.Len(tester, event.Errors, 0) } diff --git a/model/unauthorized.go b/model/unauthorized.go new file mode 100644 index 00000000..70c339ce --- /dev/null +++ b/model/unauthorized.go @@ -0,0 +1,35 @@ +// 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 model + +import ( + "fmt" + "time" +) + +type Unauthorized struct { + CreateTime time.Time + Subject string + Operation string + Target string +} + +func NewUnauthorized(subject string, operation string, target string) *Unauthorized { + return &Unauthorized{ + CreateTime: time.Now(), + Subject: subject, + Operation: operation, + Target: target, + } +} + +func (err *Unauthorized) Error() string { + return fmt.Sprintf("Subject '%v' is not authorized to perform operation '%v' on target '%v' @ '%v'", err.Subject, err.Operation, err.Target, err.CreateTime) +} diff --git a/model/unauthorized_test.go b/model/unauthorized_test.go new file mode 100644 index 00000000..6deb9edc --- /dev/null +++ b/model/unauthorized_test.go @@ -0,0 +1,24 @@ +// 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 model + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewUnauthorized(tester *testing.T) { + event := NewUnauthorized("mysubject", "myop", "mytarget") + assert.NotZero(tester, event.CreateTime) + assert.Equal(tester, event.Subject, "mysubject") + assert.Equal(tester, event.Operation, "myop") + assert.Equal(tester, event.Target, "mytarget") +} diff --git a/model/user.go b/model/user.go index 41330f24..425e314b 100644 --- a/model/user.go +++ b/model/user.go @@ -15,24 +15,24 @@ import ( ) type User struct { - Id string `json:"id"` - CreateTime time.Time `json:"createTime"` - UpdateTime time.Time `json:"updateTime"` - Email string `json:"email"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Role string `json:"role"` - Status string `json:"status"` - SearchUsername string `json:"searchUsername"` + Id string `json:"id"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` + Email string `json:"email"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Roles []string `json:"roles"` + Status string `json:"status"` + SearchUsername string `json:"searchUsername"` } func NewUser() *User { return &User{ - CreateTime: time.Now(), - Email: "", - FirstName: "", - LastName: "", - Status: "", + CreateTime: time.Now(), + Email: "", + FirstName: "", + LastName: "", + Status: "", SearchUsername: "", } } diff --git a/rbac/permissions b/rbac/permissions new file mode 100644 index 00000000..e2f2c326 --- /dev/null +++ b/rbac/permissions @@ -0,0 +1,16 @@ +# Define low-level permissions and which permission set roles can use them +# Syntax => permX: roleY roleZ +# Explanation => roleY and roleZ are granted permission permX + +grid/read: grid-monitor +roles/read: user-monitor +roles/write: user-admin +users/read: user-monitor +users/write: user-admin + + +# Define low-level permission set inheritence relationships +# Syntax => roleB: roleA +# Explanation => roleA inherits all of roleA's permissions + +user-monitor: user-admin \ No newline at end of file diff --git a/rbac/roles b/rbac/roles new file mode 100644 index 00000000..50879843 --- /dev/null +++ b/rbac/roles @@ -0,0 +1,7 @@ +# Define which business level roles can access which permission set roles. +# Syntax => roleX: roleY roleZ +# Explanation => roleY and roleZ are granted permissions of roleX + +grid-monitor: superuser analyst +user-monitor: analyst +user-admin: superuser \ No newline at end of file diff --git a/server/modules/influxdb/influxdbmetrics.go b/server/modules/influxdb/influxdbmetrics.go index 8d7047dd..da17f527 100644 --- a/server/modules/influxdb/influxdbmetrics.go +++ b/server/modules/influxdb/influxdbmetrics.go @@ -12,21 +12,21 @@ package influxdb import ( "context" "crypto/tls" - "strconv" - "sync" - "time" "github.com/apex/log" "github.com/influxdata/influxdb-client-go/v2" "github.com/influxdata/influxdb-client-go/v2/api" "github.com/security-onion-solutions/securityonion-soc/model" + "strconv" + "sync" + "time" ) type InfluxDBMetrics struct { - client influxdb2.Client - token string - org string - bucket string - queryApi api.QueryAPI + client influxdb2.Client + token string + org string + bucket string + queryApi api.QueryAPI cacheLock sync.Mutex cacheExpirationMs int @@ -43,21 +43,21 @@ type InfluxDBMetrics struct { func NewInfluxDBMetrics() *InfluxDBMetrics { return &InfluxDBMetrics{ - raidStatus: make(map[string]int), - processStatus: make(map[string]int), + raidStatus: make(map[string]int), + processStatus: make(map[string]int), consumptionEps: make(map[string]int), - productionEps: make(map[string]int), - failedEvents: make(map[string]int), + productionEps: make(map[string]int), + failedEvents: make(map[string]int), } } -func (metrics *InfluxDBMetrics) Init(hostUrl string, - token string, - org string, - bucket string, - verifyCert bool, - cacheExpirationMs int, - maxMetricAgeSeconds int) error { +func (metrics *InfluxDBMetrics) Init(hostUrl string, + token string, + org string, + bucket string, + verifyCert bool, + cacheExpirationMs int, + maxMetricAgeSeconds int) error { options := influxdb2.DefaultOptions() options.SetTLSConfig(&tls.Config{ InsecureSkipVerify: !verifyCert, @@ -86,13 +86,13 @@ func (metrics *InfluxDBMetrics) fetchLatestValuesByHost(measurement string, fiel if metrics.client != nil { log.WithFields(log.Fields{ "measurement": measurement, - "field": field, + "field": field, }).Debug("Fetching latest values by host") - result, err := metrics.queryApi.Query(context.Background(), - `from(bucket:"` + metrics.bucket + `") - |> range(start: -` + strconv.Itoa(metrics.maxMetricAgeSeconds) + `s) - |> filter(fn: (r) => r._measurement == "` + measurement + `" - and r._field == "` + field + `") + result, err := metrics.queryApi.Query(context.Background(), + `from(bucket:"`+metrics.bucket+`") + |> range(start: -`+strconv.Itoa(metrics.maxMetricAgeSeconds)+`s) + |> filter(fn: (r) => r._measurement == "`+measurement+`" + and r._field == "`+field+`") |> group(columns: ["host"]) |> last()`) if err == nil { for result.Next() { @@ -122,7 +122,7 @@ func (metrics *InfluxDBMetrics) convertValuesToString(values map[string]interfac if str, ok := v.(string); ok { results[k] = str } else { - log.WithFields(log.Fields { "key": k, "value": v,}).Warn("Unexpected value type; expected string") + log.WithFields(log.Fields{"key": k, "value": v}).Warn("Unexpected value type; expected string") } } return results @@ -138,7 +138,7 @@ func (metrics *InfluxDBMetrics) convertValuesToInt(values map[string]interface{} } else if num, ok := v.(int); ok { results[k] = num } else { - log.WithFields(log.Fields { "key": k, "value": v,}).Warn("Unexpected value type; expected float64, int64, or int") + log.WithFields(log.Fields{"key": k, "value": v}).Warn("Unexpected value type; expected float64, int64, or int") } } return results @@ -191,14 +191,16 @@ func (metrics *InfluxDBMetrics) getRaidStatus(host string) string { if hostStatus, exists := metrics.raidStatus[host]; exists { switch hostStatus { - case 0: status = model.NodeStatusOk - case 1: status = model.NodeStatusFault + case 0: + status = model.NodeStatusOk + case 1: + status = model.NodeStatusFault } } else { - log.WithFields(log.Fields { - "host": host, + log.WithFields(log.Fields{ + "host": host, "raidStatus": metrics.raidStatus, - }).Warn("Host not found in raid status metrics") + }).Info("Host not found in raid status metrics") } return status @@ -211,12 +213,14 @@ func (metrics *InfluxDBMetrics) getProcessStatus(host string) string { if hostStatus, exists := metrics.processStatus[host]; exists { switch hostStatus { - case 0: status = model.NodeStatusOk - case 1: status = model.NodeStatusFault + case 0: + status = model.NodeStatusOk + case 1: + status = model.NodeStatusFault } } else { - log.WithFields(log.Fields { - "host": host, + log.WithFields(log.Fields{ + "host": host, "processStatus": metrics.processStatus, }).Warn("Host not found in process status metrics") } @@ -262,4 +266,4 @@ func (metrics *InfluxDBMetrics) UpdateNodeMetrics(node *model.Node) bool { enhancedStatusEnabled := (metrics.client != nil) return node.UpdateOverallStatus(enhancedStatusEnabled) -} \ No newline at end of file +} diff --git a/server/modules/kratos/kratos.go b/server/modules/kratos/kratos.go index 4bf02404..92c3947d 100644 --- a/server/modules/kratos/kratos.go +++ b/server/modules/kratos/kratos.go @@ -14,18 +14,16 @@ import ( "github.com/security-onion-solutions/securityonion-soc/server" ) -const DEFAULT_CACHE_MS = 60000 - type Kratos struct { - config module.ModuleConfig - server *server.Server - impl *KratosUserstore + config module.ModuleConfig + server *server.Server + impl *KratosUserstore } func NewKratos(srv *server.Server) *Kratos { - return &Kratos { + return &Kratos{ server: srv, - impl: NewKratosUserstore(), + impl: NewKratosUserstore(srv), } } @@ -35,10 +33,9 @@ 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, cacheMs) + err = kratos.impl.Init(url) if err == nil { kratos.server.Userstore = kratos.impl err = kratos.server.Host.AddPreprocessor(NewKratosPreprocessor(kratos.impl)) diff --git a/server/modules/kratos/kratospreprocessor.go b/server/modules/kratos/kratospreprocessor.go index dfb1c59a..3fce5a64 100644 --- a/server/modules/kratos/kratospreprocessor.go +++ b/server/modules/kratos/kratospreprocessor.go @@ -36,7 +36,8 @@ func (proc *KratosPreprocessor) Preprocess(ctx context.Context, request *http.Re userId := request.Header.Get("x-user-id") if userId != "" { - user, err := proc.userstore.GetUser(userId) + ctx = context.WithValue(ctx, web.ContextKeyRequestorId, userId) + user, err := proc.userstore.GetUser(ctx, userId) if err == nil { ctx = context.WithValue(ctx, web.ContextKeyRequestor, user) } diff --git a/server/modules/kratos/kratospreprocessor_test.go b/server/modules/kratos/kratospreprocessor_test.go index f5bf60f7..19369ee4 100644 --- a/server/modules/kratos/kratospreprocessor_test.go +++ b/server/modules/kratos/kratospreprocessor_test.go @@ -10,12 +10,12 @@ package kratos import ( - "context" - "net/http" - "testing" - "time" - "github.com/security-onion-solutions/securityonion-soc/model" - "github.com/security-onion-solutions/securityonion-soc/web" + "context" + "github.com/security-onion-solutions/securityonion-soc/fake" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/web" + "net/http" + "testing" ) func TestPreprocessPriority(tester *testing.T) { @@ -25,15 +25,28 @@ func TestPreprocessPriority(tester *testing.T) { } } func TestPreprocess(tester *testing.T) { - expectedId := "112233" + 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() + user := model.NewUser() + user.Id = expectedId + userstore := NewKratosUserstore(fake.NewAuthorizedServer(make(map[string][]string))) + userstore.Init("some/url") + kratosUsersResponseJson := ` + [ + { + "credentials": {}, + "id": "112233", + "recovery_addresses": [], + "state": "active", + "traits": { + "email": "", + "firstname": "", + "lastname": "" + }, + "verifiable_addresses": [] + } + ]` + userstore.client.MockStringResponse(kratosUsersResponseJson, 200, nil) handler := NewKratosPreprocessor(userstore) request, _ := http.NewRequest("GET", "", nil) @@ -42,21 +55,29 @@ func TestPreprocess(tester *testing.T) { ctx, statusCode, err := handler.Preprocess(context.Background(), request) if err != nil { - tester.Errorf("Unexpected error: %v", err) + tester.Errorf("Unexpected error: %v", err) } if statusCode != 0 { - tester.Errorf("expected 0 statusCode but got %d", statusCode) + tester.Errorf("expected 0 statusCode but got %d", statusCode) } if ctx == nil { - tester.Errorf("Unexpected nil context return") + tester.Errorf("Unexpected nil context return") } requestor := ctx.Value(web.ContextKeyRequestor) if requestor == nil { - tester.Errorf("Expected non-nil requestor") + tester.Errorf("Expected non-nil requestor") } actualId := requestor.(*model.User).Id if actualId != expectedId { tester.Errorf("expected %s but got %s", expectedId, actualId) } + + requestorId := ctx.Value(web.ContextKeyRequestorId) + if requestorId == nil { + tester.Errorf("Expected non-nil requestor ID") + } + if requestorId != expectedId { + tester.Errorf("expected %s but got %s", expectedId, requestorId) + } } diff --git a/server/modules/kratos/kratosuser.go b/server/modules/kratos/kratosuser.go index f30e04c8..cd4b884c 100644 --- a/server/modules/kratos/kratosuser.go +++ b/server/modules/kratos/kratosuser.go @@ -9,36 +9,34 @@ package kratos import ( - "time" "github.com/security-onion-solutions/securityonion-soc/model" + "time" ) 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"` + Status string `json:"status"` } -func NewTraits(email string, firstName string, lastName string, role string, status string) *KratosTraits { - traits := &KratosTraits { - Email: email, +func NewTraits(email string, firstName string, lastName string, status string) *KratosTraits { + traits := &KratosTraits{ + Email: email, FirstName: firstName, - LastName: lastName, - Role: role, - Status: status, + LastName: lastName, + Status: status, } return traits } 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,38 +53,36 @@ func NewAddresses(email string) []*KratosAddress { } type KratosUser struct { - Id string `json:"id"` - SchemaId string `json:"schema_id"` - SchemaUrl string `json:"schema_url"` - Traits *KratosTraits `json:"traits"` - Addresses []*KratosAddress `json:"verifiable_addresses"` + Id string `json:"id"` + SchemaId string `json:"schema_id"` + SchemaUrl string `json:"schema_url"` + Traits *KratosTraits `json:"traits"` + Addresses []*KratosAddress `json:"verifiable_addresses"` } -func NewKratosUser(email string, firstName string, lastName string, role string, status string) *KratosUser { +func NewKratosUser(email string, firstName string, lastName string, status string) *KratosUser { kratosUser := &KratosUser{ - Traits: NewTraits(email, firstName, lastName, role, status), + Traits: NewTraits(email, firstName, lastName, status), Addresses: NewAddresses(email), } return kratosUser } -func (kratosUser* KratosUser) copyToUser(user *model.User) { +func (kratosUser *KratosUser) copyToUser(user *model.User) { user.Id = kratosUser.Id user.Email = kratosUser.Traits.Email user.FirstName = kratosUser.Traits.FirstName user.LastName = kratosUser.Traits.LastName - user.Role = kratosUser.Traits.Role user.Status = kratosUser.Traits.Status } -func (kratosUser* KratosUser) copyFromUser(user *model.User) { +func (kratosUser *KratosUser) copyFromUser(user *model.User) { if kratosUser.Traits == nil { kratosUser.Traits = &KratosTraits{} } kratosUser.Traits.Email = user.Email kratosUser.Traits.FirstName = user.FirstName kratosUser.Traits.LastName = user.LastName - kratosUser.Traits.Role = user.Role kratosUser.Traits.Status = user.Status if len(kratosUser.Addresses) == 0 { kratosUser.Addresses = make([]*KratosAddress, 1) @@ -94,4 +90,4 @@ func (kratosUser* KratosUser) copyFromUser(user *model.User) { } kratosUser.Addresses[0].Value = user.Email kratosUser.Addresses[0].Verified = true -} \ No newline at end of file +} diff --git a/server/modules/kratos/kratosuser_test.go b/server/modules/kratos/kratosuser_test.go index a91649e4..0fc54764 100644 --- a/server/modules/kratos/kratosuser_test.go +++ b/server/modules/kratos/kratosuser_test.go @@ -10,59 +10,52 @@ package kratos import ( - "testing" - "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/model" + "testing" ) func TestCopyFromUser(tester *testing.T) { - kratosUser := &KratosUser{} - user := model.NewUser() - user.Email = "my@email" - user.FirstName = "myFirstname" - user.LastName = "myLastname" - user.Role = "myRole" + kratosUser := &KratosUser{} + user := model.NewUser() + user.Email = "my@email" + user.FirstName = "myFirstname" + user.LastName = "myLastname" user.Status = "locked" kratosUser.copyFromUser(user) if kratosUser.Traits.Email != user.Email { tester.Errorf("Email failed to convert") - } + } if kratosUser.Traits.FirstName != user.FirstName { tester.Errorf("FirstName failed to convert") - } + } if kratosUser.Traits.LastName != user.LastName { tester.Errorf("LastName 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.Addresses[0].Value != user.Email { tester.Errorf("Address failed to convert") - } + } } func TestCopyToUser(tester *testing.T) { - kratosUser := NewKratosUser("myEmail", "myFirst", "myLast", "myRole", "locked") - user := model.NewUser() - kratosUser.copyToUser(user) + kratosUser := NewKratosUser("myEmail", "myFirst", "myLast", "locked") + user := model.NewUser() + kratosUser.copyToUser(user) if kratosUser.Traits.Email != user.Email { tester.Errorf("Email failed to convert") - } + } if kratosUser.Traits.FirstName != user.FirstName { tester.Errorf("FirstName failed to convert") - } + } if kratosUser.Traits.LastName != user.LastName { tester.Errorf("LastName 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.Addresses[0].Value != user.Email { tester.Errorf("Address failed to convert") - } -} \ No newline at end of file + } +} diff --git a/server/modules/kratos/kratosuserstore.go b/server/modules/kratos/kratosuserstore.go index fb371725..4af52204 100644 --- a/server/modules/kratos/kratosuserstore.go +++ b/server/modules/kratos/kratosuserstore.go @@ -10,75 +10,94 @@ package kratos import ( - "sync" - "time" + "context" "github.com/apex/log" "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/server" "github.com/security-onion-solutions/securityonion-soc/web" ) type KratosUserstore struct { - client *web.Client - cacheMs time.Duration - cacheLock sync.Mutex - users []*model.User - usersLastUpdated time.Time + server *server.Server + client *web.Client } -func NewKratosUserstore() *KratosUserstore { - return &KratosUserstore { +func NewKratosUserstore(server *server.Server) *KratosUserstore { + return &KratosUserstore{ + server: server, } } -func (kratos* KratosUserstore) Init(url string, cacheMs int) error { +func (kratos *KratosUserstore) Init(url string) error { kratos.client = web.NewClient(url, true) - kratos.cacheMs = time.Duration(cacheMs) * time.Millisecond return nil } -func (kratos* KratosUserstore) fetchUser(id string) (*KratosUser, error) { +func (kratos *KratosUserstore) fetchUser(id string) (*KratosUser, error) { kratosUser := &KratosUser{} - _, err := kratos.client.SendObject("GET", "/identities/" + id, "", &kratosUser, false) + _, err := kratos.client.SendObject("GET", "/identities/"+id, "", &kratosUser, false) return kratosUser, err } -func (kratos *KratosUserstore) GetUsers() ([]*model.User, error) { - kratos.cacheLock.Lock() - defer kratos.cacheLock.Unlock() +func (kratos *KratosUserstore) GetUsers(ctx context.Context) ([]*model.User, error) { + kratosUsers := make([]*KratosUser, 0, 0) - if time.Now().Sub(kratos.usersLastUpdated) > kratos.cacheMs { - kratosUsers := make([]*KratosUser, 0, 0) + if err := kratos.server.Authorizer.CheckContextOperationAuthorized(ctx, "read", "users"); err != nil { + // User is only allowed to get their own user. Even though the user is already on + // the context we have to fetch it again to ensure it's fully updated with the + // latest user attributes. + + if requestorId, ok := ctx.Value(web.ContextKeyRequestorId).(string); ok { + log.WithFields(log.Fields{ + "requestorId": requestorId, + "requestId": ctx.Value(web.ContextKeyRequestId), + }).Debug("Fetching own user for requestor ID") + + var kratosUser KratosUser + _, err = kratos.client.SendObject("GET", "/identities/"+requestorId, "", &kratosUser, false) + if err != nil { + log.WithError(err).WithField("userId", requestorId).Error("Failed to fetch user from Kratos") + return nil, err + } + kratosUsers = append(kratosUsers, &kratosUser) + } else { + // Missing context data, unlikely to occur + return nil, err + } + } else { + // User is allowed to view all users, go get them _, 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 kratos.users, nil + + // Convert the kratos users to SOC users + users := make([]*model.User, 0, 0) + for _, kratosUser := range kratosUsers { + user := model.NewUser() + kratosUser.copyToUser(user) + kratos.server.Rolestore.PopulateUserRoles(ctx, user) + users = append(users, user) + } + return users, nil } func (kratos *KratosUserstore) DeleteUser(id string) error { log.WithField("id", id).Debug("Deleting user") - _, err := kratos.client.SendObject("DELETE", "/identities/" + id, "", nil, false) + _, err := kratos.client.SendObject("DELETE", "/identities/"+id, "", nil, false) if err != nil { log.WithError(err).Error("Failed to delete user from Kratos") } return err } -func (kratos *KratosUserstore) GetUser(id string) (*model.User, error) { +func (kratos *KratosUserstore) GetUser(ctx context.Context, id string) (*model.User, error) { var err error var user *model.User - users, err := kratos.GetUsers() + users, err := kratos.GetUsers(ctx) if err == nil { for _, testUser := range users { if testUser.Id == id { @@ -96,10 +115,10 @@ func (kratos *KratosUserstore) UpdateUser(id string, user *model.User) error { log.WithError(err).Error("Original user not found") } else { kratosUser.copyFromUser(user) - _, err = kratos.client.SendObject("PUT", "/identities/" + id, kratosUser, nil, false) + _, err = kratos.client.SendObject("PUT", "/identities/"+id, kratosUser, nil, false) if err != nil { log.WithError(err).Error("Failed to update user in Kratos") - } + } } return err } diff --git a/server/modules/kratos/kratosuserstore_test.go b/server/modules/kratos/kratosuserstore_test.go index 07080a2e..d45ea32c 100644 --- a/server/modules/kratos/kratosuserstore_test.go +++ b/server/modules/kratos/kratosuserstore_test.go @@ -10,13 +10,34 @@ package kratos import ( + "context" + "errors" + "github.com/security-onion-solutions/securityonion-soc/fake" + "github.com/security-onion-solutions/securityonion-soc/model" "testing" ) func TestUserstoreInit(tester *testing.T) { - ai := NewKratosUserstore() - err := ai.Init("abc", 1) + ai := NewKratosUserstore(nil) + err := ai.Init("abc") if err != nil { tester.Errorf("unexpected Init error") - } -} \ No newline at end of file + } +} + +func TestUnauthorized(tester *testing.T) { + userStore := NewKratosUserstore(fake.NewUnauthorizedServer()) + + _, err := userStore.GetUsers(context.Background()) + ensureUnauthorized(tester, err) + + _, err = userStore.GetUser(context.Background(), "some-id") + ensureUnauthorized(tester, err) +} + +func ensureUnauthorized(tester *testing.T, err error) { + var authErr *model.Unauthorized + if err == nil || !errors.As(err, &authErr) { + tester.Errorf("Expected unauthorized error but got %v", err) + } +} diff --git a/server/modules/modules.go b/server/modules/modules.go index 6ecfc8d1..b203b75d 100644 --- a/server/modules/modules.go +++ b/server/modules/modules.go @@ -13,12 +13,13 @@ package modules import ( "github.com/security-onion-solutions/securityonion-soc/module" "github.com/security-onion-solutions/securityonion-soc/server" + "github.com/security-onion-solutions/securityonion-soc/server/modules/elastic" "github.com/security-onion-solutions/securityonion-soc/server/modules/filedatastore" "github.com/security-onion-solutions/securityonion-soc/server/modules/influxdb" "github.com/security-onion-solutions/securityonion-soc/server/modules/kratos" - "github.com/security-onion-solutions/securityonion-soc/server/modules/elastic" "github.com/security-onion-solutions/securityonion-soc/server/modules/sostatus" "github.com/security-onion-solutions/securityonion-soc/server/modules/statickeyauth" + "github.com/security-onion-solutions/securityonion-soc/server/modules/staticrbac" "github.com/security-onion-solutions/securityonion-soc/server/modules/thehive" ) @@ -30,6 +31,7 @@ func BuildModuleMap(srv *server.Server) map[string]module.Module { moduleMap["elastic"] = elastic.NewElastic(srv) moduleMap["sostatus"] = sostatus.NewSoStatus(srv) moduleMap["statickeyauth"] = statickeyauth.NewStaticKeyAuth(srv) + moduleMap["staticrbac"] = staticrbac.NewStaticRbac(srv) moduleMap["thehive"] = thehive.NewTheHive(srv) return moduleMap } diff --git a/server/modules/modules_test.go b/server/modules/modules_test.go index 0b345253..49f99681 100644 --- a/server/modules/modules_test.go +++ b/server/modules/modules_test.go @@ -11,8 +11,8 @@ package modules import ( - "testing" "github.com/security-onion-solutions/securityonion-soc/module" + "testing" ) func TestBuildModuleMap(tester *testing.T) { diff --git a/server/modules/staticrbac/rbac_permissions.test b/server/modules/staticrbac/rbac_permissions.test new file mode 100644 index 00000000..065f550b --- /dev/null +++ b/server/modules/staticrbac/rbac_permissions.test @@ -0,0 +1,5 @@ +some/action:somerole:+ +another/action:anotherrole thirdrole,fourthrole; fifthrole +another/action:thirdrole:- +foo/bar:thirdrole +roles/read:user-monitor \ No newline at end of file diff --git a/server/modules/staticrbac/rbac_roles.test b/server/modules/staticrbac/rbac_roles.test new file mode 100644 index 00000000..538655f1 --- /dev/null +++ b/server/modules/staticrbac/rbac_roles.test @@ -0,0 +1,9 @@ +# comment:here +malformed line +thirdrole:user +fourthrole:analyst +analyst: superuser +user-monitor:user + +analyst: some@where.invalid +user: some@one.invalid \ No newline at end of file diff --git a/server/modules/staticrbac/staticrbac.go b/server/modules/staticrbac/staticrbac.go new file mode 100644 index 00000000..b28b0f62 --- /dev/null +++ b/server/modules/staticrbac/staticrbac.go @@ -0,0 +1,63 @@ +// 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 staticrbac + +import ( + "github.com/security-onion-solutions/securityonion-soc/module" + "github.com/security-onion-solutions/securityonion-soc/server" +) + +const DEFAULT_SCAN_INTERVAL_MS = 60000 + +type StaticRbac struct { + config module.ModuleConfig + server *server.Server + impl *StaticRbacAuthorizer +} + +func NewStaticRbac(srv *server.Server) *StaticRbac { + return &StaticRbac{ + server: srv, + impl: NewStaticRbacAuthorizer(), + } +} + +func (auth *StaticRbac) PrerequisiteModules() []string { + return nil +} + +func (auth *StaticRbac) Init(cfg module.ModuleConfig) error { + auth.config = cfg + + paths, err := module.GetStringArray(cfg, "roleFiles") + if err == nil { + scanIntervalMs := module.GetIntDefault(cfg, "scanIntervalMs", DEFAULT_SCAN_INTERVAL_MS) + err = auth.impl.Init(paths, scanIntervalMs) + if err == nil { + auth.server.Rolestore = auth.impl + auth.server.Authorizer = auth.impl + } + } + return err +} + +func (auth *StaticRbac) Start() error { + auth.impl.StartScanningFiles() + return nil +} + +func (auth *StaticRbac) Stop() error { + auth.impl.StopScanningFiles() + return nil +} + +func (auth *StaticRbac) IsRunning() bool { + return auth.impl.running +} diff --git a/server/modules/staticrbac/staticrbac_test.go b/server/modules/staticrbac/staticrbac_test.go new file mode 100644 index 00000000..274adc77 --- /dev/null +++ b/server/modules/staticrbac/staticrbac_test.go @@ -0,0 +1,34 @@ +// 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 staticrbac + +import ( + "github.com/security-onion-solutions/securityonion-soc/config" + "github.com/security-onion-solutions/securityonion-soc/module" + "github.com/security-onion-solutions/securityonion-soc/server" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestInit(tester *testing.T) { + scfg := &config.ServerConfig{} + srv := server.NewServer(scfg, "") + auth := NewStaticRbac(srv) + cfg := make(module.ModuleConfig) + err := auth.Init(cfg) + assert.Error(tester, err) + + array := make([]interface{}, 1, 1) + array[0] = "MyValue1" + cfg["roleFiles"] = array + err = auth.Init(cfg) + assert.NoError(tester, err) + assert.NotNil(tester, auth.server.Authorizer) +} diff --git a/server/modules/staticrbac/staticrbacauthorizer.go b/server/modules/staticrbac/staticrbacauthorizer.go new file mode 100644 index 00000000..004e3f35 --- /dev/null +++ b/server/modules/staticrbac/staticrbacauthorizer.go @@ -0,0 +1,235 @@ +// 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 staticrbac + +import ( + "bufio" + "context" + "crypto/md5" + "errors" + "github.com/apex/log" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/web" + "os" + "sort" + "strings" + "sync" + "time" +) + +type StaticRbacAuthorizer struct { + roleFiles []string + scanIntervalMs int + roleMap map[string][]string + mutex sync.Mutex + running bool + previousHash [16]byte + timer *time.Timer +} + +func NewStaticRbacAuthorizer() *StaticRbacAuthorizer { + return &StaticRbacAuthorizer{} +} + +func (impl *StaticRbacAuthorizer) Init(files []string, scanIntervalMs int) error { + impl.roleFiles = files + + if scanIntervalMs == 0 { + return errors.New("scanIntervalMs must be a positive integer") + } + impl.scanIntervalMs = scanIntervalMs + return nil +} + +func (impl *StaticRbacAuthorizer) StartScanningFiles() { + impl.running = true + go impl.scanLoop() +} + +func (impl *StaticRbacAuthorizer) StopScanningFiles() { + impl.running = false + impl.timer.Stop() +} + +func (impl *StaticRbacAuthorizer) GetAssignments(ctx context.Context) (map[string][]string, error) { + roleMap := impl.roleMap + + if err := impl.CheckContextOperationAuthorized(ctx, "read", "roles"); err != nil { + // User is not allowed to access the entire role map, so only show their top-level roles + roleMap = make(map[string][]string) + + if user, ok := ctx.Value(web.ContextKeyRequestor).(*model.User); ok { + roles := impl.roleMap[user.Email] + roleMap[user.Email] = roles + log.WithFields(log.Fields{ + "email": user.Email, + "roles": roles, + }).Debug("User does not have access to read all roles; limiting role map to self") + } + } + + return roleMap, nil +} + +func (impl *StaticRbacAuthorizer) PopulateUserRoles(ctx context.Context, user *model.User) error { + // Use the returned roles instead of the struct roles so that they are filtered for access permissions + roleMap, _ := impl.GetAssignments(ctx) + + if roles, ok := roleMap[user.Email]; ok { + sort.Strings(roles) + user.Roles = roles + log.WithFields(log.Fields{ + "email": user.Email, + "roles": user.Roles, + }).Debug("Populated roles for user") + } else { + log.WithField("email", user.Email).Debug("No roles found") + } + return nil +} + +func (impl *StaticRbacAuthorizer) UpdateRoleMap(newRoleMap map[string][]string) { + impl.mutex.Lock() + defer impl.mutex.Unlock() + + impl.roleMap = newRoleMap +} + +func (impl *StaticRbacAuthorizer) AdjustPermissionInRole(roleMap map[string][]string, role string, permission string, operation string) { + perms := roleMap[role] + if perms == nil { + perms = make([]string, 0, 0) + } + if operation == "+" { + perms = append(perms, permission) + } else if operation == "-" { + for idx, value := range perms { + if value == permission { + perms = append(perms[:idx], perms[idx+1:]...) + break + } + } + } + roleMap[role] = perms +} + +func (impl *StaticRbacAuthorizer) CheckContextOperationAuthorized(ctx context.Context, operation string, target string) error { + var err error + permission := target + "/" + operation + + if user, ok := ctx.Value(web.ContextKeyRequestor).(*model.User); ok { + log.WithFields(log.Fields{ + "userId": user.Id, + "username": user.Email, + "requestId": ctx.Value(web.ContextKeyRequestId), + "permission": permission, + "roleMap": impl.roleMap, + }).Info("Evaluating authorization for requestor") + + impl.mutex.Lock() + defer impl.mutex.Unlock() + + if user.Email == permission { + err = errors.New("Unable to check authorization of a subject name that matches the permission name itself") + } + if !impl.isAuthorized(user.Email, permission) { + err = model.NewUnauthorized(user.Email, operation, target) + } + } else { + log.Debug("Authorization user not found in context") + err = model.NewUnauthorized("", operation, target) + } + return err +} + +func (impl *StaticRbacAuthorizer) isAuthorized(subject string, requestedPermission string) bool { + if subject == requestedPermission { + return true + } + + if permissions, ok := impl.roleMap[subject]; ok { + for _, allowedPermission := range permissions { + if impl.isAuthorized(allowedPermission, requestedPermission) { + return true + } + } + } + + return false +} + +func (impl *StaticRbacAuthorizer) scanLoop() { + log.WithField("scanIntervalMs", impl.scanIntervalMs).Info("Starting periodic role file scanner") + for impl.running { + impl.scanFiles() + <-impl.timer.C + } + log.Info("Stopped scanning role files") +} + +func (impl *StaticRbacAuthorizer) scanFiles() { + log.Debug("Scanning role files for updates") + newRoleMap := make(map[string][]string) + hashText := "" + for lineNum, path := range impl.roleFiles { + file, err := os.Open(path) + if err != nil { + log.WithError(err).WithField("path", path).Error("Unable to open role file") + } else { + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + hashText = hashText + line + impl.parseLine(newRoleMap, line, path, lineNum) + } + } + } + + hash := md5.Sum([]byte(hashText)) + if hash != impl.previousHash { + log.Info("Role files have changed; updating roles") + impl.UpdateRoleMap(newRoleMap) + impl.previousHash = hash + } + + impl.timer = time.NewTimer(time.Millisecond * time.Duration(impl.scanIntervalMs)) +} + +func (impl *StaticRbacAuthorizer) parseLine(roleMap map[string][]string, line string, path string, lineNum int) { + line = strings.ReplaceAll(line, ",", " ") // Allow comma delimiting + line = strings.ReplaceAll(line, ";", " ") // Allow semi-colon delimiting + line = strings.TrimSpace(line) + + if len(line) > 0 && !strings.HasPrefix(line, "#") { + pieces := strings.Split(line, ":") + if len(pieces) < 2 || len(pieces) > 3 { + log.WithFields(log.Fields{ + "lineNumber": lineNum + 1, + "filepath": path, + }).Warn("Invalid role mapping found while parsing role file") + } else { + permission := strings.TrimSpace(pieces[0]) + roles := strings.Split(pieces[1], " ") + operation := "+" + if len(pieces) > 2 { + operation = pieces[2] + } + + for _, role := range roles { + role = strings.TrimSpace(role) + if len(role) > 0 { + impl.AdjustPermissionInRole(roleMap, role, permission, operation) + } + } + } + } +} diff --git a/server/modules/staticrbac/staticrbacauthorizer_test.go b/server/modules/staticrbac/staticrbacauthorizer_test.go new file mode 100644 index 00000000..222f28dd --- /dev/null +++ b/server/modules/staticrbac/staticrbacauthorizer_test.go @@ -0,0 +1,144 @@ +// 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 staticrbac + +import ( + "context" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/web" + "github.com/stretchr/testify/assert" + "testing" +) + +func prepareTest(tester *testing.T, email string) (*StaticRbacAuthorizer, context.Context, *model.User) { + ctx := context.Background() + user := model.NewUser() + user.Email = email + ctx = context.WithValue(ctx, web.ContextKeyRequestor, user) + + auth := NewStaticRbacAuthorizer() + roleFiles := []string{"rbac_permissions.test", "rbac_roles.test"} + auth.Init(roleFiles, DEFAULT_SCAN_INTERVAL_MS) + + assert.Equal(tester, DEFAULT_SCAN_INTERVAL_MS, auth.scanIntervalMs) + assert.Equal(tester, roleFiles, auth.roleFiles) + + auth.scanFiles() + + return auth, ctx, user +} + +func TestCheckContextOperationAuthorized_EmptyContext(tester *testing.T) { + ctx := context.Background() + auth := NewStaticRbacAuthorizer() + err := auth.CheckContextOperationAuthorized(ctx, "myop", "mytarget") + assert.Error(tester, err, "Expected error due to missing context data") +} + +func TestCheckContextOperationAuthorized_Collision(tester *testing.T) { + ctx := context.Background() + user := model.NewUser() + user.Email = "mytarget/myop" + ctx = context.WithValue(ctx, web.ContextKeyRequestor, user) + + auth := NewStaticRbacAuthorizer() + err := auth.CheckContextOperationAuthorized(ctx, "myop", "mytarget") + assert.Error(tester, err) +} + +func TestCheckContextOperationAuthorized_Fail(tester *testing.T) { + ctx := context.Background() + ctx = context.WithValue(ctx, web.ContextKeyRequestor, model.NewUser()) + + auth := NewStaticRbacAuthorizer() + err := auth.CheckContextOperationAuthorized(ctx, "myop", "mytarget") + var unauthErr *model.Unauthorized + assert.ErrorAs(tester, err, &unauthErr) +} + +func TestCheckContextOperationAuthorized_FailRemoved(tester *testing.T) { + auth, ctx, _ := prepareTest(tester, "some@one.invalid") + + err := auth.CheckContextOperationAuthorized(ctx, "bar", "foo") + assert.NoError(tester, err) + + err = auth.CheckContextOperationAuthorized(ctx, "action", "another") + var unauthErr *model.Unauthorized + assert.ErrorAs(tester, err, &unauthErr) +} + +func TestCheckContextOperationAuthorized_Success(tester *testing.T) { + auth, ctx, _ := prepareTest(tester, "some@where.invalid") + + err := auth.CheckContextOperationAuthorized(ctx, "action", "another") + assert.NoError(tester, err) + + err = auth.CheckContextOperationAuthorized(ctx, "action", "some") + var unauthErr *model.Unauthorized + assert.ErrorAs(tester, err, &unauthErr) +} + +func TestIsAuthorized(tester *testing.T) { + auth := NewStaticRbacAuthorizer() + + roleMap := make(map[string][]string) + roleMap["clerk"] = []string{"register/operates", "tables/maintains"} + roleMap["baker"] = []string{"cakes/bake", "icing/decorates"} + roleMap["chef"] = []string{"recipes/create", "menus/create"} + roleMap["henry"] = []string{"baker"} + roleMap["tom"] = []string{"chef"} + roleMap["alice"] = []string{} + + auth.UpdateRoleMap(roleMap) + + var testTable = []struct { + subject string + permission string + authorized bool + }{ + {"henry", "cakes/bake", true}, + {"henry", "pies/bake", false}, + {"henry", "register/operates", false}, + {"alice", "pies/bake", false}, + {"alice", "cakes/bake", false}, + {"alice", "register/operates", false}, + {"tom", "cakes/bake", false}, + {"tom", "recipes/create", true}, + {"tom", "register/operates", false}, + } + + for _, test := range testTable { + tester.Run("subject="+test.subject+", permission="+test.permission, func(t *testing.T) { + actual := auth.isAuthorized(test.subject, test.permission) + assert.Equal(tester, test.authorized, actual) + }) + } +} + +func TestGetAssignments_Self(tester *testing.T) { + auth, ctx, user := prepareTest(tester, "some@one.invalid") + + roleMap, err := auth.GetAssignments(ctx) + assert.NoError(tester, err) + assert.Contains(tester, roleMap, user.Email) + + var expectedRoles = [...]string{"user"} + assert.ElementsMatch(tester, expectedRoles, roleMap[user.Email]) +} + +func TestPopulateUserRoles(tester *testing.T) { + auth, ctx, user := prepareTest(tester, "some@one.invalid") + + err := auth.PopulateUserRoles(ctx, user) + assert.NoError(tester, err) + + var expectedRoles = [...]string{"user"} + assert.ElementsMatch(tester, expectedRoles, user.Roles) +} diff --git a/server/roleshandler.go b/server/roleshandler.go new file mode 100644 index 00000000..a2656730 --- /dev/null +++ b/server/roleshandler.go @@ -0,0 +1,50 @@ +// 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 server + +import ( + "context" + "errors" + "github.com/security-onion-solutions/securityonion-soc/web" + "net/http" +) + +type RolesHandler struct { + web.BaseHandler + server *Server +} + +func NewRolesHandler(srv *Server) *RolesHandler { + handler := &RolesHandler{} + handler.Host = srv.Host + handler.server = srv + handler.Impl = handler + return handler +} + +func (rolesHandler *RolesHandler) HandleNow(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { + if rolesHandler.server.Rolestore == nil { + return http.StatusMethodNotAllowed, nil, errors.New("Roles module not enabled") + } + + switch request.Method { + case http.MethodGet: + return rolesHandler.get(ctx, writer, request) + } + return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") +} + +func (rolesHandler *RolesHandler) get(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { + roles, err := rolesHandler.server.Rolestore.GetAssignments(ctx) + if err != nil { + return http.StatusBadRequest, nil, err + } + return http.StatusOK, roles, nil +} diff --git a/server/rolestore.go b/server/rolestore.go new file mode 100644 index 00000000..e11e4655 --- /dev/null +++ b/server/rolestore.go @@ -0,0 +1,21 @@ +// 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 server + +import ( + "context" + "github.com/security-onion-solutions/securityonion-soc/model" +) + +type Rolestore interface { + GetAssignments(ctx context.Context) (map[string][]string, error) + PopulateUserRoles(ctx context.Context, user *model.User) error +} diff --git a/server/server.go b/server/server.go index 77b442ac..d34b0cff 100644 --- a/server/server.go +++ b/server/server.go @@ -11,28 +11,35 @@ package server import ( - "os/exec" - "strings" + "context" "github.com/apex/log" "github.com/security-onion-solutions/securityonion-soc/config" "github.com/security-onion-solutions/securityonion-soc/web" + "os/exec" + "strings" ) +type Authorizer interface { + CheckContextOperationAuthorized(ctx context.Context, operation string, target string) error +} + type Server struct { - Config *config.ServerConfig - Host *web.Host - Datastore Datastore + Config *config.ServerConfig + Host *web.Host + Datastore Datastore Userstore Userstore + Rolestore Rolestore Eventstore Eventstore Casestore Casestore Metrics Metrics stoppedChan chan bool + Authorizer Authorizer } func NewServer(cfg *config.ServerConfig, version string) *Server { return &Server{ - Config: cfg, - Host: web.NewHost(cfg.BindAddress, cfg.HtmlDir, cfg.IdleConnectionTimeoutMs, version), + Config: cfg, + Host: web.NewHost(cfg.BindAddress, cfg.HtmlDir, cfg.IdleConnectionTimeoutMs, version), stoppedChan: make(chan bool, 1), } } @@ -70,7 +77,7 @@ func (server *Server) Stop() { } func (server *Server) Wait() { - <- server.stoppedChan + <-server.stoppedChan } func (server *Server) GetTimezones() []string { @@ -87,4 +94,4 @@ func (server *Server) GetTimezones() []string { log.WithError(err).Error("Unable to lookup timezones from operating system") } return zones -} \ No newline at end of file +} diff --git a/server/userhandler.go b/server/userhandler.go index 6c8656ee..3aca5755 100644 --- a/server/userhandler.go +++ b/server/userhandler.go @@ -48,7 +48,7 @@ func (userHandler *UserHandler) get(ctx context.Context, writer http.ResponseWri if !safe { return http.StatusBadRequest, nil, errors.New("Invalid id") } - user, err := userHandler.server.Userstore.GetUser(id) + user, err := userHandler.server.Userstore.GetUser(ctx, id) if err != nil { return http.StatusBadRequest, nil, err } diff --git a/server/usershandler.go b/server/usershandler.go index 0c2117bd..91a9a433 100644 --- a/server/usershandler.go +++ b/server/usershandler.go @@ -42,7 +42,7 @@ func (usersHandler *UsersHandler) HandleNow(ctx context.Context, writer http.Res } func (usersHandler *UsersHandler) get(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { - users, err := usersHandler.server.Userstore.GetUsers() + users, err := usersHandler.server.Userstore.GetUsers(ctx) if err != nil { return http.StatusBadRequest, nil, err } diff --git a/server/userstore.go b/server/userstore.go index 287c6cb5..c8d71e29 100644 --- a/server/userstore.go +++ b/server/userstore.go @@ -11,12 +11,13 @@ package server import ( + "context" "github.com/security-onion-solutions/securityonion-soc/model" ) type Userstore interface { - GetUsers() ([]*model.User, error) + GetUsers(ctx context.Context) ([]*model.User, error) DeleteUser(id string) error - GetUser(id string) (*model.User, error) + GetUser(ctx context.Context, id string) (*model.User, error) UpdateUser(id string, user *model.User) error } \ No newline at end of file diff --git a/web/basehandler.go b/web/basehandler.go index d9d78a6f..2e0c3be5 100644 --- a/web/basehandler.go +++ b/web/basehandler.go @@ -15,11 +15,13 @@ import ( "compress/gzip" "context" "encoding/json" + "errors" "net/http" "reflect" "strings" "time" "github.com/apex/log" + "github.com/security-onion-solutions/securityonion-soc/model" ) type HandlerImpl interface { @@ -57,7 +59,10 @@ func (handler *BaseHandler) Handle(responseWriter http.ResponseWriter, request * "requestor": context.Value(ContextKeyRequestor), }).Warn("Request did not complete successfully") - if statusCode < http.StatusBadRequest { + var unauthorizedError *model.Unauthorized + if errors.As(err, &unauthorizedError) { + statusCode = http.StatusUnauthorized + } else if statusCode < http.StatusBadRequest { statusCode = http.StatusInternalServerError } responseWriter.WriteHeader(statusCode) diff --git a/web/basehandler_test.go b/web/basehandler_test.go index 66313924..f10fcafa 100644 --- a/web/basehandler_test.go +++ b/web/basehandler_test.go @@ -10,6 +10,7 @@ package web import ( + "github.com/stretchr/testify/assert" "strconv" "testing" ) @@ -19,37 +20,35 @@ type TestHandler struct { } func NewTestHandler() *TestHandler { - handler := &TestHandler {} + handler := &TestHandler{} return handler } func TestGetPathParameter(tester *testing.T) { handler := NewTestHandler() var testTable = []struct { - path string - index int + path string + index int expected string - } { - { "", -1, "" }, - { "", 0, "" }, - { "", 1, "" }, - { "/", -1, "" }, - { "/", 0, "" }, - { "/", 1, "" }, - { "/123", -1, "" }, - { "/123", 0, "123" }, - { "/123", 1, "" }, - { "/123/", 0, "123" }, - { "/123/", 1, "" }, - { "/123/456", 0, "123" }, - { "/123/456", 1, "456" }, + }{ + {"", -1, ""}, + {"", 0, ""}, + {"", 1, ""}, + {"/", -1, ""}, + {"/", 0, ""}, + {"/", 1, ""}, + {"/123", -1, ""}, + {"/123", 0, "123"}, + {"/123", 1, ""}, + {"/123/", 0, "123"}, + {"/123/", 1, ""}, + {"/123/456", 0, "123"}, + {"/123/456", 1, "456"}, } for _, test := range testTable { - tester.Run("path=" + test.path + ", index=" + strconv.Itoa(test.index), func(t *testing.T) { - actual := handler.GetPathParameter(test.path, test.index) - if actual != test.expected { - t.Errorf("expected %s but got %s", test.expected, actual) - } + tester.Run("path="+test.path+", index="+strconv.Itoa(test.index), func(t *testing.T) { + actual := handler.GetPathParameter(test.path, test.index) + assert.Equal(tester, test.expected, actual) }) } } diff --git a/web/basepreprocessor.go b/web/basepreprocessor.go index 263aa523..4d84a20b 100644 --- a/web/basepreprocessor.go +++ b/web/basepreprocessor.go @@ -18,6 +18,7 @@ import ( type ContextKey string const ContextKeyRequestId = ContextKey("ContextKeyRequestId") +const ContextKeyRequestorId = ContextKey("ContextKeyRequestorId") const ContextKeyRequestor = ContextKey("ContextKeyRequestor") type BasePreprocessor struct { diff --git a/web/basepreprocessor_test.go b/web/basepreprocessor_test.go index 23e8b69b..8f5cc85b 100644 --- a/web/basepreprocessor_test.go +++ b/web/basepreprocessor_test.go @@ -11,29 +11,23 @@ package web import ( "context" + "github.com/stretchr/testify/assert" "net/http" "testing" ) func TestPreprocessPriority(tester *testing.T) { handler := NewBasePreprocessor() - if handler.PreprocessPriority() != 0 { - tester.Error("expected 0 priority") - } + assert.Zero(tester, handler.PreprocessPriority()) } 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") - } + assert.NoError(tester, err) + assert.Zero(tester, statusCode) + actualId := ctx.Value(ContextKeyRequestId).(string) - if len(actualId) != 36 { - tester.Errorf("Expected a valid UUID but got %s", actualId) - } + assert.Len(tester, actualId, 36, "Expected valid UUID") } diff --git a/web/client.go b/web/client.go index b35e08da..e515afe9 100644 --- a/web/client.go +++ b/web/client.go @@ -15,12 +15,12 @@ import ( "crypto/tls" "encoding/json" "errors" + "github.com/apex/log" "io" "io/ioutil" "net/http" "strconv" "strings" - "github.com/apex/log" ) type ClientAuth interface { @@ -28,13 +28,16 @@ type ClientAuth interface { } type Client struct { - Auth ClientAuth - hostUrl string - impl *http.Client + Auth ClientAuth + hostUrl string + impl *http.Client + mock bool + mockResponse *http.Response + mockError error } func NewClient(url string, verifyCert bool) *Client { - client := &Client { + client := &Client{ hostUrl: url, } transport := &http.Transport{ @@ -44,6 +47,22 @@ func NewClient(url string, verifyCert bool) *Client { return client } +func (client *Client) MockStringResponse(body string, statusCode int, mockError error) { + mockResp := &http.Response{ + Body: ioutil.NopCloser(bytes.NewBufferString(body)), + StatusCode: statusCode, + ContentLength: int64(len(body)), + Header: make(http.Header, 0), + } + client.MockResponse(mockResp, mockError) +} + +func (client *Client) MockResponse(mockResponse *http.Response, mockError error) { + client.mock = true + client.mockResponse = mockResponse + client.mockError = mockError +} + func (client *Client) SendAuthorizedObject(method string, path string, obj interface{}, returnedObj interface{}) (bool, error) { return client.SendObject(method, path, obj, returnedObj, true) } @@ -59,7 +78,7 @@ func (client *Client) SendObject(method string, path string, obj interface{}, re if resp.StatusCode < 200 || resp.StatusCode > 299 { bytes, _ := ioutil.ReadAll(resp.Body) body := string(bytes) - log.WithFields(log.Fields { + log.WithFields(log.Fields{ "body": body, }).Debug("Response") err = errors.New("Request did not complete successfully (" + strconv.Itoa(resp.StatusCode) + "): " + resp.Status) @@ -95,23 +114,28 @@ func (client *Client) SendRequest(method string, path string, contentType string if auth { err = client.Auth.Authorize(req) if err == nil { - log.WithFields(log.Fields { - "url": formattedUrl, + log.WithFields(log.Fields{ + "url": formattedUrl, "method": method, }).Debug("Sending authorized request") } } if err == nil { - resp, err = client.impl.Do(req) + if client.mock { + resp = client.mockResponse + err = client.mockError + } else { + resp, err = client.impl.Do(req) + } if err != nil { log.WithError(err).Warn("Failed to submit request") } else { - log.WithFields(log.Fields { - "url": formattedUrl, - "method": method, - "statusCode": resp.StatusCode, - "status": resp.Status, + log.WithFields(log.Fields{ + "url": formattedUrl, + "method": method, + "statusCode": resp.StatusCode, + "status": resp.Status, "contentLength": resp.ContentLength, }).Info("HTTP request finished") } @@ -126,4 +150,4 @@ func (client *Client) FormatUrl(url string, path string) string { formattedUrl = formattedUrl + "/" formattedUrl = formattedUrl + strings.TrimPrefix(path, "/") return formattedUrl -} \ No newline at end of file +} diff --git a/web/client_test.go b/web/client_test.go index 05163498..c9adc830 100644 --- a/web/client_test.go +++ b/web/client_test.go @@ -11,29 +11,41 @@ package web import ( + "github.com/stretchr/testify/assert" "testing" ) func TestFormatUrl(tester *testing.T) { client := NewClient("http://some.where/path", true) var testTable = []struct { - url string - path string + url string + path string expected string - } { - { "http://far.out", "path", "http://far.out/path" }, - { "http://far.out", "/path", "http://far.out/path" }, - { "http://far.out/", "path", "http://far.out/path" }, - { "http://far.out/", "/path", "http://far.out/path" }, - { "http://far.out/", "/path/end", "http://far.out/path/end" }, + }{ + {"http://far.out", "path", "http://far.out/path"}, + {"http://far.out", "/path", "http://far.out/path"}, + {"http://far.out/", "path", "http://far.out/path"}, + {"http://far.out/", "/path", "http://far.out/path"}, + {"http://far.out/", "/path/end", "http://far.out/path/end"}, } for _, test := range testTable { - tester.Run("url=" + test.url + ", path=" + test.path, func(t *testing.T) { - actual := client.FormatUrl(test.url, test.path) - if actual != test.expected { - t.Errorf("expected %s but got %s", test.expected, actual) - } + tester.Run("url="+test.url+", path="+test.path, func(t *testing.T) { + actual := client.FormatUrl(test.url, test.path) + assert.Equal(tester, test.expected, actual) }) } -} \ No newline at end of file +} + +type TestObject struct { + Foo string +} + +func TestMock(tester *testing.T) { + client := NewClient("http://some.where/path", true) + respObj := &TestObject{} + respBody := `{"foo": "bar"}` + client.MockStringResponse(respBody, 200, nil) + client.SendObject("GET", "subpath", nil, respObj, false) + assert.Equal(tester, "bar", respObj.Foo) +} diff --git a/web/connection_test.go b/web/connection_test.go index 8e71d77b..09004585 100644 --- a/web/connection_test.go +++ b/web/connection_test.go @@ -11,26 +11,22 @@ package web import ( - "testing" - "time" + "github.com/stretchr/testify/assert" + "testing" + "time" ) func TestIsAuthorized(tester *testing.T) { conn := NewConnection(nil, "") - result := conn.IsAuthorized("test") - if !result { - tester.Errorf("expected connection to be authorized for message %s", "test") - } + assert.True(tester, conn.IsAuthorized("test")) } func TestUpdatePingTime(tester *testing.T) { - conn := NewConnection(nil, "") + conn := NewConnection(nil, "") oldPingTime := conn.lastPingTime time.Sleep(3 * time.Millisecond) conn.UpdatePingTime() newPingTime := conn.lastPingTime - if newPingTime.Sub(oldPingTime).Milliseconds() < 3 { - tester.Errorf("expected increase in lastPingTime from %v, but got %v", oldPingTime, newPingTime) - } -} \ No newline at end of file + assert.True(tester, newPingTime.Sub(oldPingTime).Milliseconds() >= 3, "expected 3s increase in lastPingTime") +} diff --git a/web/host_test.go b/web/host_test.go index 12b7c513..c202d415 100644 --- a/web/host_test.go +++ b/web/host_test.go @@ -12,64 +12,49 @@ package web import ( "context" - "net/http" - "testing" - "time" "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" + "net/http" + "testing" + "time" ) func TestAddRemoveConnection(tester *testing.T) { - host := NewHost("http://some.where/path", "/tmp/foo", 123, "unit test") + host := NewHost("http://some.where/path", "/tmp/foo", 123, "unit test") conn := &websocket.Conn{} tester.Run("testing add connection", func(t *testing.T) { - socConn := host.AddConnection(conn, "1.2.3.4"); - if len(host.connections) != 1 { - tester.Errorf("expected %d but got %d", 1, len(host.connections)) - } - - if socConn.ip != "1.2.3.4" { - tester.Errorf("expected %s but got %s", "1.2.3.4", socConn.ip) - } - - if host.idleConnectionTimeoutMs != 123 { - tester.Errorf("expected %d but got %d", 123, host.idleConnectionTimeoutMs) - } + socConn := host.AddConnection(conn, "1.2.3.4") + assert.Len(tester, host.connections, 1) + assert.Equal(tester, socConn.ip, "1.2.3.4", socConn.ip) + assert.Equal(tester, 123, host.idleConnectionTimeoutMs) }) tester.Run("testing remove connection", func(t *testing.T) { - host.RemoveConnection(conn); - if len(host.connections) != 0 { - t.Errorf("final expected %d but got %d", 0, len(host.connections)) - } + host.RemoveConnection(conn) + assert.Len(tester, host.connections, 0) }) } func TestMultipleConnections(tester *testing.T) { - host := NewHost("http://some.where/path", "/tmp/foo", 123, "unit test") + host := NewHost("http://some.where/path", "/tmp/foo", 123, "unit test") conn1 := &websocket.Conn{} conn2 := &websocket.Conn{} tester.Run("testing add multiple connections", func(t *testing.T) { - host.AddConnection(conn1, "1.2.3.4"); - host.AddConnection(conn2, "1.2.3.4"); - if len(host.connections) != 2 { - tester.Errorf("expected %d but got %d", 2, len(host.connections)) - } + host.AddConnection(conn1, "1.2.3.4") + host.AddConnection(conn2, "1.2.3.4") + assert.Len(tester, host.connections, 2) }) tester.Run("testing remove first connection", func(t *testing.T) { - host.RemoveConnection(conn1); - if len(host.connections) != 1 { - t.Errorf("final expected %d but got %d", 1, len(host.connections)) - } + host.RemoveConnection(conn1) + assert.Len(tester, host.connections, 1) }) tester.Run("testing remove second connection", func(t *testing.T) { - host.RemoveConnection(conn2); - if len(host.connections) != 0 { - t.Errorf("final expected %d but got %d", 0, len(host.connections)) - } + host.RemoveConnection(conn2) + assert.Len(tester, host.connections, 0) }) } func TestManageConnections(tester *testing.T) { - host := NewHost("http://some.where/path", "/tmp/foo", 123, "unit test") + host := NewHost("http://some.where/path", "/tmp/foo", 123, "unit test") conn := host.AddConnection(nil, "") conn.lastPingTime = time.Time{} @@ -82,28 +67,23 @@ func TestManageConnections(tester *testing.T) { host.running = true host.manageConnections(10 * time.Millisecond) - if len(host.connections) != 0 { - tester.Errorf("Expected no connections after manage cycle") - } + assert.Len(tester, host.connections, 0) } func TestGetSourceIp(tester *testing.T) { - host := NewHost("http://some.where/path", "/tmp/foo", 123, "unit test") - request, _ := http.NewRequest("GET", "", nil) + host := NewHost("http://some.where/path", "/tmp/foo", 123, "unit test") + request, _ := http.NewRequest("GET", "", nil) - expected := "1.1.1.1" - request.Header.Set("X-real-IP", expected) + expected := "1.1.1.1" + request.Header.Set("X-real-IP", expected) - actual := host.GetSourceIp(request) - if actual != expected { - tester.Errorf("expected %s but got %s", expected, actual) - } + assert.Equal(tester, expected, host.GetSourceIp(request)) } type DummyPreprocessor struct { - priority int + priority int statusCode int - err error + err error } func (dummy *DummyPreprocessor) PreprocessPriority() int { @@ -116,56 +96,35 @@ func (dummy *DummyPreprocessor) Preprocess(ctx context.Context, request *http.Re 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") - } + assert.Len(tester, host.Preprocessors(), 1) - newPreprocessor := &DummyPreprocessor{ priority: 123, } + newPreprocessor := &DummyPreprocessor{priority: 123} host.AddPreprocessor(newPreprocessor) - if len(host.Preprocessors()) != 2 { - tester.Errorf("expected two preprocessors on host") - } + assert.Len(tester, host.Preprocessors(), 2) - if host.Preprocessors()[1] != newPreprocessor { - tester.Errorf("expected new preprocessors to be processed last") - } + assert.Equal(tester, newPreprocessor, host.Preprocessors()[1], "expected new preprocessors to be processed last") - newPreprocessor2 := &DummyPreprocessor{ priority: 12, } + 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") - } + assert.Len(tester, host.Preprocessors(), 3) + assert.Equal(tester, newPreprocessor2, host.Preprocessors()[1], "expected new preprocessors to be processed second") // Should collide - newPreprocessor3 := &DummyPreprocessor{ priority: 12, } + 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") - } + assert.Error(tester, err, "Expected error from colliding preprocessors") + assert.Len(tester, host.Preprocessors(), 3) } func TestPreprocessExecute(tester *testing.T) { host := NewHost("http://some.where/path", "/tmp/foo", 123, "unit test") - newPreprocessor := &DummyPreprocessor{ priority: 123, statusCode: 321} + 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 + assert.NoError(tester, err) + assert.Equal(tester, 321, statusCode) + assert.NotNil(tester, ctx.Value(ContextKeyRequestId), "Context mismatch after preprocessing") +} From ee7aedfe69dcc266defc3fa366d29343e2bf911e Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Fri, 3 Sep 2021 09:24:43 -0400 Subject: [PATCH 06/32] Avoid logging raid warnings when there is no raid data for the cluster --- server/modules/influxdb/influxdbmetrics.go | 24 ++++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/server/modules/influxdb/influxdbmetrics.go b/server/modules/influxdb/influxdbmetrics.go index da17f527..fdc47f9d 100644 --- a/server/modules/influxdb/influxdbmetrics.go +++ b/server/modules/influxdb/influxdbmetrics.go @@ -189,18 +189,20 @@ func (metrics *InfluxDBMetrics) getRaidStatus(host string) string { metrics.updateRaidStatus() - if hostStatus, exists := metrics.raidStatus[host]; exists { - switch hostStatus { - case 0: - status = model.NodeStatusOk - case 1: - status = model.NodeStatusFault + if len(metrics.raidStatus) > 0 { + if hostStatus, exists := metrics.raidStatus[host]; exists { + switch hostStatus { + 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") } - } else { - log.WithFields(log.Fields{ - "host": host, - "raidStatus": metrics.raidStatus, - }).Info("Host not found in raid status metrics") } return status From adcd7d0cab6423bcd8d1476c26020c251db5cc8d Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Fri, 3 Sep 2021 09:50:21 -0400 Subject: [PATCH 07/32] Include perm files in image --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 7af95ae3..3ce259ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,7 @@ WORKDIR /opt/sensoroni COPY --from=builder /build/sensoroni . COPY --from=builder /build/scripts ./scripts COPY --from=builder /build/html ./html +COPY --from=builder /build/rbac ./rbac COPY --from=builder /build/COPYING . COPY --from=builder /build/LICENSE . COPY --from=builder /build/README.md . From fd039932ce20dcee0da124e2350a9c1ce2bbafb8 Mon Sep 17 00:00:00 2001 From: William Wernert Date: Fri, 3 Sep 2021 12:26:00 -0400 Subject: [PATCH 08/32] Refactor mergeElasticUpdateResults() for clarity Rework above function as a method of the EventuUpdateResults struct that modifies the struct in the same way the first arg was changed in the original function. i.e. mergeElasticUpdateResults(result1, result2) -> becomes result1.AddEventUpdateResults(result2) -> --- model/event.go | 201 ++++---- server/modules/elastic/converter.go | 587 +++++++++++------------ server/modules/elastic/converter_test.go | 19 +- 3 files changed, 401 insertions(+), 406 deletions(-) diff --git a/model/event.go b/model/event.go index 8a9b18ee..30d6451d 100644 --- a/model/event.go +++ b/model/event.go @@ -10,162 +10,167 @@ package model import ( - "strconv" - "strings" - "time" - "github.com/apex/log" + "strconv" + "strings" + "time" + + "github.com/apex/log" ) type EventResults struct { - CreateTime time.Time `json:"createTime"` - CompleteTime time.Time `json:"completeTime"` - ElapsedMs int `json:"elapsedMs"` - Errors []string `json:"errors"` + CreateTime time.Time `json:"createTime"` + CompleteTime time.Time `json:"completeTime"` + ElapsedMs int `json:"elapsedMs"` + Errors []string `json:"errors"` } func (results *EventResults) initEventResults() { - results.CreateTime = time.Now() - results.Errors = make([]string, 0) + results.CreateTime = time.Now() + results.Errors = make([]string, 0) } func (results *EventResults) Complete() { - results.CompleteTime = time.Now() + results.CompleteTime = time.Now() } type EventSearchResults struct { - EventResults - Criteria *EventSearchCriteria `json:"criteria"` - TotalEvents int `json:"totalEvents"` - Events []*EventRecord `json:"events"` - Metrics map[string]([]*EventMetric) `json:"metrics"` + EventResults + Criteria *EventSearchCriteria `json:"criteria"` + TotalEvents int `json:"totalEvents"` + Events []*EventRecord `json:"events"` + Metrics map[string]([]*EventMetric) `json:"metrics"` } func NewEventSearchResults() *EventSearchResults { - results := &EventSearchResults{ - Events: make([]*EventRecord, 0, 0), - Metrics: make(map[string]([]*EventMetric)), - } - results.initEventResults() - return results + results := &EventSearchResults{ + Events: make([]*EventRecord, 0, 0), + Metrics: make(map[string]([]*EventMetric)), + } + results.initEventResults() + return results } type EventSearchCriteria struct { - RawQuery string `json:"query"` - DateRange string `json:"dateRange"` - MetricLimit int `json:"metricLimit"` - EventLimit int `json:"eventLimit"` - BeginTime time.Time - EndTime time.Time - CreateTime time.Time - ParsedQuery *Query + RawQuery string `json:"query"` + DateRange string `json:"dateRange"` + MetricLimit int `json:"metricLimit"` + EventLimit int `json:"eventLimit"` + BeginTime time.Time + EndTime time.Time + CreateTime time.Time + ParsedQuery *Query } func (criteria *EventSearchCriteria) initSearchCriteria() { - criteria.CreateTime = time.Now() - criteria.ParsedQuery = NewQuery() - criteria.EventLimit = 25 - criteria.MetricLimit = 10 + criteria.CreateTime = time.Now() + criteria.ParsedQuery = NewQuery() + criteria.EventLimit = 25 + criteria.MetricLimit = 10 } func NewEventSearchCriteria() *EventSearchCriteria { - criteria := &EventSearchCriteria{} - criteria.initSearchCriteria() - return criteria + criteria := &EventSearchCriteria{} + criteria.initSearchCriteria() + return criteria } func (criteria *EventSearchCriteria) Populate(query string, dateRange string, dateRangeFormat string, timezone string, metricLimit string, eventLimit string) error { - var err error - criteria.RawQuery = strings.Trim(query, " ") + var err error + criteria.RawQuery = strings.Trim(query, " ") - datePieces := strings.SplitN(dateRange, " - ", 2) + datePieces := strings.SplitN(dateRange, " - ", 2) - loc, err := time.LoadLocation(timezone) - if err != nil { - log.WithField("timezone", timezone).Info("Invalid timezone provided by client") - loc, _ = time.LoadLocation("UTC") - } + loc, err := time.LoadLocation(timezone) + if err != nil { + log.WithField("timezone", timezone).Info("Invalid timezone provided by client") + loc, _ = time.LoadLocation("UTC") + } - if len(datePieces) == 2 { - criteria.BeginTime, err = time.ParseInLocation(dateRangeFormat, strings.Trim(datePieces[0], " "), loc) + if len(datePieces) == 2 { + criteria.BeginTime, err = time.ParseInLocation(dateRangeFormat, strings.Trim(datePieces[0], " "), loc) - if err == nil { - criteria.EndTime, err = time.ParseInLocation(dateRangeFormat, strings.Trim(datePieces[1], " "), loc) - } - } else { - criteria.DateRange = "" - criteria.EndTime = time.Now() - criteria.BeginTime = criteria.EndTime.Add(time.Duration(-24) * time.Hour) - } + if err == nil { + criteria.EndTime, err = time.ParseInLocation(dateRangeFormat, strings.Trim(datePieces[1], " "), loc) + } + } else { + criteria.DateRange = "" + criteria.EndTime = time.Now() + criteria.BeginTime = criteria.EndTime.Add(time.Duration(-24) * time.Hour) + } - if err == nil { - criteria.MetricLimit, err = strconv.Atoi(metricLimit) - } - if err == nil { - criteria.EventLimit, err = strconv.Atoi(eventLimit) - } + if err == nil { + criteria.MetricLimit, err = strconv.Atoi(metricLimit) + } + if err == nil { + criteria.EventLimit, err = strconv.Atoi(eventLimit) + } - if err == nil { - err = criteria.ParsedQuery.Parse(query) - } + if err == nil { + err = criteria.ParsedQuery.Parse(query) + } - return err + return err } type EventMetric struct { - Keys []interface{} `json:"keys"` - Value int `json:"value"` + Keys []interface{} `json:"keys"` + Value int `json:"value"` } type EventRecord struct { - Source string `json:"source"` - Timestamp string `json:"timestamp"` - Id string `json:"id"` - Type string `json:"type"` - Score float64 `json:"score"` - Payload map[string]interface{} `json:"payload"` + Source string `json:"source"` + Timestamp string `json:"timestamp"` + Id string `json:"id"` + Type string `json:"type"` + Score float64 `json:"score"` + Payload map[string]interface{} `json:"payload"` } type EventUpdateCriteria struct { - EventSearchCriteria - UpdateScripts []string `json:"updateScripts"` - Asynchronous bool `json:"async"` + EventSearchCriteria + UpdateScripts []string `json:"updateScripts"` + Asynchronous bool `json:"async"` } func NewEventUpdateCriteria() *EventUpdateCriteria { - criteria := &EventUpdateCriteria{} - criteria.initSearchCriteria() - return criteria + criteria := &EventUpdateCriteria{} + criteria.initSearchCriteria() + return criteria } func (criteria *EventUpdateCriteria) AddUpdateScript(script string) { - criteria.UpdateScripts = append(criteria.UpdateScripts, script) + criteria.UpdateScripts = append(criteria.UpdateScripts, script) } type EventUpdateResults struct { - EventResults - Criteria *EventUpdateCriteria `json:"criteria"` - UpdatedCount int `json:"updatedCount"` - UnchangedCount int `json:"unchangedCount"` + EventResults + Criteria *EventUpdateCriteria `json:"criteria"` + UpdatedCount int `json:"updatedCount"` + UnchangedCount int `json:"unchangedCount"` } func NewEventUpdateResults() *EventUpdateResults { - results := &EventUpdateResults { - } - results.initEventResults() - return results + results := &EventUpdateResults{} + results.initEventResults() + return results +} + +func (results *EventUpdateResults) AddEventUpdateResults(newResults *EventUpdateResults) { + results.UpdatedCount += newResults.UpdatedCount + results.UnchangedCount += newResults.UnchangedCount + results.ElapsedMs += newResults.ElapsedMs } type EventAckCriteria struct { - SearchFilter string `json:"searchFilter"` - EventFilter map[string]interface{} `json:"eventFilter"` - DateRange string `json:"dateRange"` - DateRangeFormat string `json:"dateRangeFormat"` - Timezone string `json:"timezone"` - Escalate bool `json:"escalate"` - Acknowledge bool `json:"acknowledge"` + SearchFilter string `json:"searchFilter"` + EventFilter map[string]interface{} `json:"eventFilter"` + DateRange string `json:"dateRange"` + DateRangeFormat string `json:"dateRangeFormat"` + Timezone string `json:"timezone"` + Escalate bool `json:"escalate"` + Acknowledge bool `json:"acknowledge"` } func NewEventAckCriteria() *EventAckCriteria { - return &EventAckCriteria{ - } -} \ No newline at end of file + return &EventAckCriteria{} +} diff --git a/server/modules/elastic/converter.go b/server/modules/elastic/converter.go index 5adc8f7b..7831ac8e 100644 --- a/server/modules/elastic/converter.go +++ b/server/modules/elastic/converter.go @@ -10,343 +10,338 @@ package elastic import ( - "errors" - "strings" - "time" - "github.com/security-onion-solutions/securityonion-soc/json" - "github.com/security-onion-solutions/securityonion-soc/model" + "errors" + "strings" + "time" + + "github.com/security-onion-solutions/securityonion-soc/json" + "github.com/security-onion-solutions/securityonion-soc/model" ) func makeAggregation(store *ElasticEventstore, prefix string, keys []string, count int, ascending bool) (map[string]interface{}, string) { - agg := make(map[string]interface{}) - orderFields := make(map[string]interface{}) - orderFields["_count"] = "desc" - if ascending { - orderFields["_count"] = "asc" - } - aggFields := make(map[string]interface{}) - if strings.HasSuffix(keys[0], "*") { - keys[0] = strings.TrimSuffix(keys[0], "*") - aggFields["missing"] = "__missing__" - } - - aggFields["field"] = store.mapElasticField(keys[0]) - aggFields["size"] = count - aggFields["order"] = orderFields - agg["terms"] = aggFields - - name := prefix + "|" + keys[0] - if len(keys) > 1 { - inner := make(map[string]interface{}) - innerAgg, innerName := makeAggregation(store, name, keys[1:], count, ascending) - inner[innerName] = innerAgg - agg["aggs"] = inner - } - return agg, name + agg := make(map[string]interface{}) + orderFields := make(map[string]interface{}) + orderFields["_count"] = "desc" + if ascending { + orderFields["_count"] = "asc" + } + aggFields := make(map[string]interface{}) + if strings.HasSuffix(keys[0], "*") { + keys[0] = strings.TrimSuffix(keys[0], "*") + aggFields["missing"] = "__missing__" + } + + aggFields["field"] = store.mapElasticField(keys[0]) + aggFields["size"] = count + aggFields["order"] = orderFields + agg["terms"] = aggFields + + name := prefix + "|" + keys[0] + if len(keys) > 1 { + inner := make(map[string]interface{}) + innerAgg, innerName := makeAggregation(store, name, keys[1:], count, ascending) + inner[innerName] = innerAgg + agg["aggs"] = inner + } + return agg, name } func makeTimeline(interval string) map[string]interface{} { - timeline := make(map[string]interface{}) - timelineFields := make(map[string]interface{}) - timelineFields["field"] = "@timestamp" - timelineFields["fixed_interval"] = interval - timelineFields["min_doc_count"] = 1 - timeline["date_histogram"] = timelineFields - return timeline + timeline := make(map[string]interface{}) + timelineFields := make(map[string]interface{}) + timelineFields["field"] = "@timestamp" + timelineFields["fixed_interval"] = interval + timelineFields["min_doc_count"] = 1 + timeline["date_histogram"] = timelineFields + return timeline } func formatSearch(input string) string { - input = strings.Trim(input, " ") - if len(input) == 0 { - input = "*" - } - return input + input = strings.Trim(input, " ") + if len(input) == 0 { + input = "*" + } + return input } func mapSearch(store *ElasticEventstore, searchSegment *model.SearchSegment) *model.SearchSegment { - const delim = ":" - for _, term := range(searchSegment.Terms()) { - if strings.HasSuffix(term.Raw, delim) && !term.Grouped && !term.Quoted { - field := strings.Trim(term.Raw, delim) - newField := store.mapElasticField(field) - if newField != field { - term.Raw = newField + delim - } - } - } - return searchSegment + const delim = ":" + for _, term := range searchSegment.Terms() { + if strings.HasSuffix(term.Raw, delim) && !term.Grouped && !term.Quoted { + field := strings.Trim(term.Raw, delim) + newField := store.mapElasticField(field) + if newField != field { + term.Raw = newField + delim + } + } + } + return searchSegment } func makeQuery(store *ElasticEventstore, parsedQuery *model.Query, beginTime time.Time, endTime time.Time) map[string]interface{} { - searchString := "" - segment := parsedQuery.NamedSegment(model.SegmentKind_Search) - if segment != nil { - searchSegment := segment.(*model.SearchSegment) - searchString = mapSearch(store, searchSegment).String() - } - - queryDetails := make(map[string]interface{}) - queryDetails["query"] = formatSearch(searchString) - queryDetails["analyze_wildcard"] = true - queryDetails["default_field"] = "*" - - query := make(map[string]interface{}) - query["query_string"] = queryDetails - - timestampDetails := make(map[string]interface{}) - timestampDetails["gte"] = beginTime.Format(time.RFC3339) - timestampDetails["lte"] = endTime.Format(time.RFC3339) - timestampDetails["format"] = "strict_date_optional_time" - - timerangeDetails := make(map[string]interface{}) - timerangeDetails["@timestamp"] = timestampDetails - - timerange := make(map[string]interface{}) - timerange["range"] = timerangeDetails - - must := make([]interface{}, 0, 0) - must = append(must, query) - must = append(must, timerange) - - terms := make(map[string]interface{}) - terms["must"] = must - terms["filter"] = []interface{}{} - terms["should"] = []interface{}{} - terms["must_not"] = []interface{}{} - - clause := make(map[string]interface{}) - clause["bool"] = terms - - return clause + searchString := "" + segment := parsedQuery.NamedSegment(model.SegmentKind_Search) + if segment != nil { + searchSegment := segment.(*model.SearchSegment) + searchString = mapSearch(store, searchSegment).String() + } + + queryDetails := make(map[string]interface{}) + queryDetails["query"] = formatSearch(searchString) + queryDetails["analyze_wildcard"] = true + queryDetails["default_field"] = "*" + + query := make(map[string]interface{}) + query["query_string"] = queryDetails + + timestampDetails := make(map[string]interface{}) + timestampDetails["gte"] = beginTime.Format(time.RFC3339) + timestampDetails["lte"] = endTime.Format(time.RFC3339) + timestampDetails["format"] = "strict_date_optional_time" + + timerangeDetails := make(map[string]interface{}) + timerangeDetails["@timestamp"] = timestampDetails + + timerange := make(map[string]interface{}) + timerange["range"] = timerangeDetails + + must := make([]interface{}, 0, 0) + must = append(must, query) + must = append(must, timerange) + + terms := make(map[string]interface{}) + terms["must"] = must + terms["filter"] = []interface{}{} + terms["should"] = []interface{}{} + terms["must_not"] = []interface{}{} + + clause := make(map[string]interface{}) + clause["bool"] = terms + + return clause } func calcTimelineInterval(intervals int, beginTime time.Time, endTime time.Time) string { - difference := endTime.Sub(beginTime) - intervalSeconds := difference.Seconds() / float64(intervals) - - // Find a common interval nearest the calculated interval - if intervalSeconds <= 3 { - return "1s" - } - if intervalSeconds <= 7 { - return "5s" - } - if intervalSeconds <= 13 { - return "10s" - } - if intervalSeconds <= 23 { - return "15s" - } - if intervalSeconds <= 45 { - return "30s" - } - if intervalSeconds <= 180 { - return "1m" - } - if intervalSeconds <= 420 { - return "5m" - } - if intervalSeconds <= 780 { - return "10m" - } - if intervalSeconds <= 1380 { - return "15m" - } - if intervalSeconds <= 2700 { - return "30m" - } - if intervalSeconds <= 5400 { - return "1h" - } - if intervalSeconds <= 25200 { - return "5h" - } - if intervalSeconds <= 54000 { - return "10h" - } - if intervalSeconds <= 259200 { - return "1d" - } - if intervalSeconds <= 604800 { - return "5d" - } - if intervalSeconds <= 1296000 { - return "10d" - } - return "30d" + difference := endTime.Sub(beginTime) + intervalSeconds := difference.Seconds() / float64(intervals) + + // Find a common interval nearest the calculated interval + if intervalSeconds <= 3 { + return "1s" + } + if intervalSeconds <= 7 { + return "5s" + } + if intervalSeconds <= 13 { + return "10s" + } + if intervalSeconds <= 23 { + return "15s" + } + if intervalSeconds <= 45 { + return "30s" + } + if intervalSeconds <= 180 { + return "1m" + } + if intervalSeconds <= 420 { + return "5m" + } + if intervalSeconds <= 780 { + return "10m" + } + if intervalSeconds <= 1380 { + return "15m" + } + if intervalSeconds <= 2700 { + return "30m" + } + if intervalSeconds <= 5400 { + return "1h" + } + if intervalSeconds <= 25200 { + return "5h" + } + if intervalSeconds <= 54000 { + return "10h" + } + if intervalSeconds <= 259200 { + return "1d" + } + if intervalSeconds <= 604800 { + return "5d" + } + if intervalSeconds <= 1296000 { + return "10d" + } + return "30d" } func convertToElasticRequest(store *ElasticEventstore, criteria *model.EventSearchCriteria) (string, error) { - var err error - var esJson string - - esMap := make(map[string]interface{}) - esMap["size"] = criteria.EventLimit - esMap["query"] = makeQuery(store, criteria.ParsedQuery, criteria.BeginTime, criteria.EndTime) - - aggregations := make(map[string]interface{}) - esMap["aggs"] = aggregations - aggregations["timeline"] = makeTimeline(calcTimelineInterval(store.intervals, criteria.BeginTime, criteria.EndTime)) - - segment := criteria.ParsedQuery.NamedSegment(model.SegmentKind_GroupBy) - if segment != nil { - groupBySegment := segment.(*model.GroupBySegment) - fields := groupBySegment.Fields() - if len(fields) > 0 { - agg, name := makeAggregation(store, "groupby", fields, criteria.MetricLimit, false) - aggregations[name] = agg - aggregations["bottom"], _ = makeAggregation(store, "", fields[0:1], criteria.MetricLimit, true) - } - } - - bytes, err := json.WriteJson(esMap) - if err == nil { - esJson = string(bytes) - } - - return esJson, err + var err error + var esJson string + + esMap := make(map[string]interface{}) + esMap["size"] = criteria.EventLimit + esMap["query"] = makeQuery(store, criteria.ParsedQuery, criteria.BeginTime, criteria.EndTime) + + aggregations := make(map[string]interface{}) + esMap["aggs"] = aggregations + aggregations["timeline"] = makeTimeline(calcTimelineInterval(store.intervals, criteria.BeginTime, criteria.EndTime)) + + segment := criteria.ParsedQuery.NamedSegment(model.SegmentKind_GroupBy) + if segment != nil { + groupBySegment := segment.(*model.GroupBySegment) + fields := groupBySegment.Fields() + if len(fields) > 0 { + agg, name := makeAggregation(store, "groupby", fields, criteria.MetricLimit, false) + aggregations[name] = agg + aggregations["bottom"], _ = makeAggregation(store, "", fields[0:1], criteria.MetricLimit, true) + } + } + + bytes, err := json.WriteJson(esMap) + if err == nil { + esJson = string(bytes) + } + + return esJson, err } func parseAggregation(name string, aggObj interface{}, keys []interface{}, results *model.EventSearchResults) { - agg := aggObj.(map[string]interface{}) - buckets := agg["buckets"] - if buckets != nil { - metrics := results.Metrics[name] - if metrics == nil { - metrics = make([]*model.EventMetric,0,0) - } - for _, bucketObj := range buckets.([]interface{}) { - bucket := bucketObj.(map[string]interface{}) - metric := &model.EventMetric{} - count := bucket["doc_count"] - if count != nil { - metric.Value = int(count.(float64)) - key := bucket["key_as_string"] - if key == nil { - key = bucket["key"] - } - if key != nil { - tmpKeys := make([]interface{}, len(keys), len(keys) + 1) - copy(tmpKeys, keys) - tmpKeys = append(tmpKeys, key) - metric.Keys = tmpKeys - metrics = append(metrics, metric) - for innerName, innerAgg := range bucket { - if strings.HasPrefix(innerName, "groupby|") { - parseAggregation(innerName, innerAgg, tmpKeys, results) - } - } - } - } - } - results.Metrics[name] = metrics - } + agg := aggObj.(map[string]interface{}) + buckets := agg["buckets"] + if buckets != nil { + metrics := results.Metrics[name] + if metrics == nil { + metrics = make([]*model.EventMetric, 0, 0) + } + for _, bucketObj := range buckets.([]interface{}) { + bucket := bucketObj.(map[string]interface{}) + metric := &model.EventMetric{} + count := bucket["doc_count"] + if count != nil { + metric.Value = int(count.(float64)) + key := bucket["key_as_string"] + if key == nil { + key = bucket["key"] + } + if key != nil { + tmpKeys := make([]interface{}, len(keys), len(keys)+1) + copy(tmpKeys, keys) + tmpKeys = append(tmpKeys, key) + metric.Keys = tmpKeys + metrics = append(metrics, metric) + for innerName, innerAgg := range bucket { + if strings.HasPrefix(innerName, "groupby|") { + parseAggregation(innerName, innerAgg, tmpKeys, results) + } + } + } + } + } + results.Metrics[name] = metrics + } } func flattenKeyValue(store *ElasticEventstore, fieldMap map[string]interface{}, prefix string, value map[string]interface{}) { - for key, value := range value { - flattenedKey := prefix + key - switch value.(type) { - case map[string]interface{}: - flattenKeyValue(store, fieldMap, flattenedKey + ".", value.(map[string]interface{})) - default: - fieldMap[store.unmapElasticField(flattenedKey)] = value - } - } + for key, value := range value { + flattenedKey := prefix + key + switch value.(type) { + case map[string]interface{}: + flattenKeyValue(store, fieldMap, flattenedKey+".", value.(map[string]interface{})) + default: + fieldMap[store.unmapElasticField(flattenedKey)] = value + } + } } func flatten(store *ElasticEventstore, data map[string]interface{}) map[string]interface{} { - fieldMap := make(map[string]interface{}) - flattenKeyValue(store, fieldMap, "", data) - return fieldMap + fieldMap := make(map[string]interface{}) + flattenKeyValue(store, fieldMap, "", data) + return fieldMap } func convertFromElasticResults(store *ElasticEventstore, esJson string, results *model.EventSearchResults) error { - esResults := make(map[string]interface{}) - err := json.LoadJson([]byte(esJson), &esResults) - if esResults["took"] == nil || esResults["timed_out"] == nil || esResults["hits"] == nil { - return errors.New("Elasticsearch response is not a valid JSON search result") - } - results.ElapsedMs = int(esResults["took"].(float64)) - timedOut := esResults["timed_out"].(bool) - if timedOut { - return errors.New("Timeout while fetching results from Elasticsearch") - } - - hits := esResults["hits"].(map[string]interface{}) - switch hits["total"].(type) { - case float64: - results.TotalEvents = int(hits["total"].(float64)) - default: - total := hits["total"].(map[string]interface{}) - results.TotalEvents = int(total["value"].(float64)) - } - - records := hits["hits"].([]interface{}) - for _, record := range records { - event := &model.EventRecord{} - esRecord := record.(map[string]interface{}) - event.Source = esRecord["_index"].(string) - event.Id = esRecord["_id"].(string) - event.Type = esRecord["_type"].(string) - event.Score = esRecord["_score"].(float64) - event.Payload = flatten(store, esRecord["_source"].(map[string]interface{})) - ts, _ := time.Parse(time.RFC3339, event.Payload["@timestamp"].(string)) - event.Timestamp = ts.Format("2006-01-02T15:04:05.000Z") - results.Events = append(results.Events, event) - } - - aggs := esResults["aggregations"] - if aggs != nil { - for name, aggObj := range aggs.(map[string]interface{}) { - keys := make([]interface{}, 0, 0) - parseAggregation(name, aggObj, keys, results) - } - } - - return err + esResults := make(map[string]interface{}) + err := json.LoadJson([]byte(esJson), &esResults) + if esResults["took"] == nil || esResults["timed_out"] == nil || esResults["hits"] == nil { + return errors.New("Elasticsearch response is not a valid JSON search result") + } + results.ElapsedMs = int(esResults["took"].(float64)) + timedOut := esResults["timed_out"].(bool) + if timedOut { + return errors.New("Timeout while fetching results from Elasticsearch") + } + + hits := esResults["hits"].(map[string]interface{}) + switch hits["total"].(type) { + case float64: + results.TotalEvents = int(hits["total"].(float64)) + default: + total := hits["total"].(map[string]interface{}) + results.TotalEvents = int(total["value"].(float64)) + } + + records := hits["hits"].([]interface{}) + for _, record := range records { + event := &model.EventRecord{} + esRecord := record.(map[string]interface{}) + event.Source = esRecord["_index"].(string) + event.Id = esRecord["_id"].(string) + event.Type = esRecord["_type"].(string) + event.Score = esRecord["_score"].(float64) + event.Payload = flatten(store, esRecord["_source"].(map[string]interface{})) + ts, _ := time.Parse(time.RFC3339, event.Payload["@timestamp"].(string)) + event.Timestamp = ts.Format("2006-01-02T15:04:05.000Z") + results.Events = append(results.Events, event) + } + + aggs := esResults["aggregations"] + if aggs != nil { + for name, aggObj := range aggs.(map[string]interface{}) { + keys := make([]interface{}, 0, 0) + parseAggregation(name, aggObj, keys, results) + } + } + + return err } func convertToElasticUpdateRequest(store *ElasticEventstore, criteria *model.EventUpdateCriteria) (string, error) { - var err error - var esJson string + var err error + var esJson string - esMap := make(map[string]interface{}) - esMap["query"] = makeQuery(store, criteria.ParsedQuery, criteria.BeginTime, criteria.EndTime) + esMap := make(map[string]interface{}) + esMap["query"] = makeQuery(store, criteria.ParsedQuery, criteria.BeginTime, criteria.EndTime) - script := make(map[string]string) - script["inline"] = strings.Join(criteria.UpdateScripts, "; ") - script["lang"] = "painless" - esMap["script"] = script + script := make(map[string]string) + script["inline"] = strings.Join(criteria.UpdateScripts, "; ") + script["lang"] = "painless" + esMap["script"] = script - bytes, err := json.WriteJson(esMap) - if err == nil { - esJson = string(bytes) - } + bytes, err := json.WriteJson(esMap) + if err == nil { + esJson = string(bytes) + } - return esJson, err + return esJson, err } func convertFromElasticUpdateResults(store *ElasticEventstore, esJson string, results *model.EventUpdateResults) error { - esResults := make(map[string]interface{}) - err := json.LoadJson([]byte(esJson), &esResults) - if esResults["took"] == nil || esResults["timed_out"] == nil || esResults["updated"] == nil || esResults["noops"] == nil { - return errors.New("Elasticsearch response is not a valid JSON updated result") - } - results.ElapsedMs = int(esResults["took"].(float64)) - timedOut := esResults["timed_out"].(bool) - if timedOut { - return errors.New("Timeout while updating documents in Elasticsearch") - } - - results.UpdatedCount = int(esResults["updated"].(float64)) - results.UnchangedCount = int(esResults["noops"].(float64)) - - return err + esResults := make(map[string]interface{}) + err := json.LoadJson([]byte(esJson), &esResults) + if esResults["took"] == nil || esResults["timed_out"] == nil || esResults["updated"] == nil || esResults["noops"] == nil { + return errors.New("Elasticsearch response is not a valid JSON updated result") + } + results.ElapsedMs = int(esResults["took"].(float64)) + timedOut := esResults["timed_out"].(bool) + if timedOut { + return errors.New("Timeout while updating documents in Elasticsearch") + } + + results.UpdatedCount = int(esResults["updated"].(float64)) + results.UnchangedCount = int(esResults["noops"].(float64)) + + return err } - -func mergeElasticUpdateResults(results *model.EventUpdateResults, newResults *model.EventUpdateResults) { - results.ElapsedMs += newResults.ElapsedMs - results.UpdatedCount += newResults.UpdatedCount - results.UnchangedCount += newResults.UnchangedCount -} \ No newline at end of file diff --git a/server/modules/elastic/converter_test.go b/server/modules/elastic/converter_test.go index 2cc2125d..d8b1d563 100644 --- a/server/modules/elastic/converter_test.go +++ b/server/modules/elastic/converter_test.go @@ -14,7 +14,9 @@ import ( "io/ioutil" "testing" "time" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/stretchr/testify/assert" ) func NewTestStore() *ElasticEventstore { @@ -275,7 +277,7 @@ func TestConvertFromElasticUpdateResultsSuccess(tester *testing.T) { } } -func TestMergeElasticUpdateResults(tester *testing.T) { +func TestAddEventUpdateResults(tester *testing.T) { results1 := model.NewEventUpdateResults() results1.ElapsedMs = 100 results1.UpdatedCount = 200 @@ -284,18 +286,11 @@ func TestMergeElasticUpdateResults(tester *testing.T) { results2.ElapsedMs = 12 results2.UpdatedCount = 2 results2.UnchangedCount = 4 - mergeElasticUpdateResults(results1, results2) - if results1.ElapsedMs != 112 { - tester.Errorf("Unexpected ElapsedMs: %d", results1.ElapsedMs) - } - if results1.UpdatedCount != 202 { - tester.Errorf("Unexpected updated count: %d", results1.UpdatedCount) - } - - if results1.UnchangedCount != 404 { - tester.Errorf("Unexpected updated count: %d", results1.UnchangedCount) - } + results1.AddEventUpdateResults(results2) + assert.Equal(tester, 112, results1.ElapsedMs) + assert.Equal(tester, 202, results1.UpdatedCount) + assert.Equal(tester, 404, results1.UnchangedCount) } func validateFormatSearch(tester *testing.T, original string, expected string) { From e9097a2920b1528c89301ea09dea78252a414a9b Mon Sep 17 00:00:00 2001 From: William Wernert Date: Fri, 3 Sep 2021 12:37:34 -0400 Subject: [PATCH 09/32] Refactor legacy unit tests to use testify/assert --- agent/agent_test.go | 24 +- agent/modules/importer/importer.go | 225 +-- agent/modules/importer/importer_test.go | 84 +- agent/modules/modules_test.go | 19 +- .../statickeyauth/statickeyauth_test.go | 26 +- agent/modules/stenoquery/stenoquery_test.go | 53 +- cmd/sensoroni_test.go | 21 +- config/agentconfig_test.go | 44 +- config/clientparameters_test.go | 108 +- config/serverconfig_test.go | 40 +- json/json_test.go | 33 +- model/event_test.go | 79 +- model/job_test.go | 112 +- model/node_test.go | 285 ++-- model/query_test.go | 226 ++- model/unauthorized_test.go | 15 +- module/manager_test.go | 44 +- module/options_test.go | 175 +-- packet/parser_test.go | 49 +- server/modules/elastic/converter_test.go | 385 ++--- server/modules/elastic/elastic_test.go | 63 +- server/modules/elastic/elasticeventstore.go | 1284 ++++++++--------- .../modules/elastic/elasticeventstore_test.go | 200 +-- .../modules/elastic/elastictransport_test.go | 68 +- .../filedatastore/filedatastoreimpl_test.go | 185 +-- server/modules/influxdb/influxdb_test.go | 123 +- .../modules/influxdb/influxdbmetrics_test.go | 108 +- server/modules/kratos/kratos_test.go | 37 +- .../modules/kratos/kratospreprocessor_test.go | 79 +- server/modules/kratos/kratosuser_test.go | 66 +- server/modules/kratos/kratosuserstore_test.go | 35 +- server/modules/modules_test.go | 27 +- server/modules/sostatus/sostatus_test.go | 27 +- .../statickeyauth/statickeyauth_test.go | 19 +- .../statickeyauth/statickeyauthimpl_test.go | 64 +- web/websockethandler_test.go | 15 +- 36 files changed, 1958 insertions(+), 2489 deletions(-) diff --git a/agent/agent_test.go b/agent/agent_test.go index ee16567e..60e98eb4 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -11,21 +11,17 @@ package agent import ( - "testing" - "github.com/security-onion-solutions/securityonion-soc/config" + "testing" + + "github.com/security-onion-solutions/securityonion-soc/config" + "github.com/stretchr/testify/assert" ) func TestNewAgent(tester *testing.T) { - cfg := &config.AgentConfig{} - cfg.ServerUrl = "http://some.where" - agent := NewAgent(cfg, "") - if agent.Client == nil { - tester.Errorf("expected non-nil agent.Client") - } - if agent.JobMgr == nil { - tester.Errorf("expected non-nil agent.JobMgr") - } - if agent.stoppedChan == nil { - tester.Errorf("expected non-nil agent.stoppedChan") - } + cfg := &config.AgentConfig{} + cfg.ServerUrl = "http://some.where" + agent := NewAgent(cfg, "") + assert.NotNil(tester, agent.Client) + assert.NotNil(tester, agent.JobMgr) + assert.NotNil(tester, agent.stoppedChan) } diff --git a/agent/modules/importer/importer.go b/agent/modules/importer/importer.go index 6fd04137..f7d2ea22 100644 --- a/agent/modules/importer/importer.go +++ b/agent/modules/importer/importer.go @@ -10,18 +10,19 @@ package importer import ( - "context" - "errors" - "fmt" - "io" - "os" - "os/exec" - "time" - "github.com/apex/log" - "github.com/kennygrant/sanitize" - "github.com/security-onion-solutions/securityonion-soc/agent" - "github.com/security-onion-solutions/securityonion-soc/model" - "github.com/security-onion-solutions/securityonion-soc/module" + "context" + "errors" + "fmt" + "io" + "os" + "os/exec" + "time" + + "github.com/apex/log" + "github.com/kennygrant/sanitize" + "github.com/security-onion-solutions/securityonion-soc/agent" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/module" ) const DEFAULT_EXECUTABLE_PATH = "tcpdump" @@ -30,133 +31,133 @@ const DEFAULT_PCAP_INPUT_PATH = "/nsm/import" const DEFAULT_TIMEOUT_MS = 1200000 type Importer struct { - config module.ModuleConfig - executablePath string - pcapOutputPath string - pcapInputPath string - agent *agent.Agent - timeoutMs int + config module.ModuleConfig + executablePath string + pcapOutputPath string + pcapInputPath string + agent *agent.Agent + timeoutMs int } func NewImporter(agt *agent.Agent) *Importer { - return &Importer { - agent: agt, - } + return &Importer{ + agent: agt, + } } func (lag *Importer) PrerequisiteModules() []string { - return nil + return nil } func (importer *Importer) Init(cfg module.ModuleConfig) error { - var err error - importer.config = cfg - importer.executablePath = module.GetStringDefault(cfg, "executablePath", DEFAULT_EXECUTABLE_PATH) - importer.pcapOutputPath = module.GetStringDefault(cfg, "pcapOutputPath", DEFAULT_PCAP_OUTPUT_PATH) - importer.pcapInputPath = module.GetStringDefault(cfg, "pcapInputPath", DEFAULT_PCAP_INPUT_PATH) - importer.timeoutMs = module.GetIntDefault(cfg, "timeoutMs", DEFAULT_TIMEOUT_MS) - if importer.agent == nil { - err = errors.New("Unable to invoke JobMgr.AddJobProcessor due to nil agent") - } else { - importer.agent.JobMgr.AddJobProcessor(importer) - } - return err + var err error + importer.config = cfg + importer.executablePath = module.GetStringDefault(cfg, "executablePath", DEFAULT_EXECUTABLE_PATH) + importer.pcapOutputPath = module.GetStringDefault(cfg, "pcapOutputPath", DEFAULT_PCAP_OUTPUT_PATH) + importer.pcapInputPath = module.GetStringDefault(cfg, "pcapInputPath", DEFAULT_PCAP_INPUT_PATH) + importer.timeoutMs = module.GetIntDefault(cfg, "timeoutMs", DEFAULT_TIMEOUT_MS) + if importer.agent == nil { + err = errors.New("Unable to invoke JobMgr.AddJobProcessor due to nil agent") + } else { + importer.agent.JobMgr.AddJobProcessor(importer) + } + return err } func (importer *Importer) Start() error { - return nil + return nil } func (importer *Importer) Stop() error { - return nil + return nil } func (importer *Importer) IsRunning() bool { - return false + return false } func (importer *Importer) ProcessJob(job *model.Job, reader io.ReadCloser) (io.ReadCloser, error) { - var err error - if len(job.Filter.ImportId) == 0 { - log.WithFields(log.Fields { - "jobId": job.Id, - "importId": job.Filter.ImportId, - }).Debug("Skipping import processor due to missing importId") - return reader, nil - } else { - job.FileExtension = "pcap" - - query := importer.buildQuery(job) - - pcapInputFilepath := fmt.Sprintf("%s/%s/pcap/data.pcap", importer.pcapInputPath, job.Filter.ImportId) - pcapOutputFilepath := fmt.Sprintf("%s/%d.%s", importer.pcapOutputPath, job.Id, job.FileExtension) - - log.WithField("jobId", job.Id).Info("Processing pcap export for imported PCAP job") - - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(importer.timeoutMs) * time.Millisecond) - defer cancel() - cmd := exec.CommandContext(ctx, importer.executablePath, "-r", pcapInputFilepath, "-w", pcapOutputFilepath, query) - var output []byte - output, err = cmd.CombinedOutput() - log.WithFields(log.Fields { - "executablePath": importer.executablePath, - "query": query, - "output": string(output), - "pcapInputFilepath": pcapInputFilepath, - "pcapOutputFilepath": pcapOutputFilepath, - "err": err, - }).Debug("Executed tcpdump") - if err == nil { - var file *os.File - file, err = os.Open(pcapOutputFilepath) - if err == nil { - reader = file - } - } - } - return reader, err + var err error + if len(job.Filter.ImportId) == 0 { + log.WithFields(log.Fields{ + "jobId": job.Id, + "importId": job.Filter.ImportId, + }).Debug("Skipping import processor due to missing importId") + return reader, nil + } else { + job.FileExtension = "pcap" + + query := importer.buildQuery(job) + + pcapInputFilepath := fmt.Sprintf("%s/%s/pcap/data.pcap", importer.pcapInputPath, job.Filter.ImportId) + pcapOutputFilepath := fmt.Sprintf("%s/%d.%s", importer.pcapOutputPath, job.Id, job.FileExtension) + + log.WithField("jobId", job.Id).Info("Processing pcap export for imported PCAP job") + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(importer.timeoutMs)*time.Millisecond) + defer cancel() + cmd := exec.CommandContext(ctx, importer.executablePath, "-r", pcapInputFilepath, "-w", pcapOutputFilepath, query) + var output []byte + output, err = cmd.CombinedOutput() + log.WithFields(log.Fields{ + "executablePath": importer.executablePath, + "query": query, + "output": string(output), + "pcapInputFilepath": pcapInputFilepath, + "pcapOutputFilepath": pcapOutputFilepath, + "err": err, + }).Debug("Executed tcpdump") + if err == nil { + var file *os.File + file, err = os.Open(pcapOutputFilepath) + if err == nil { + reader = file + } + } + } + return reader, err } func (importer *Importer) CleanupJob(job *model.Job) { - pcapOutputFilepath := fmt.Sprintf("%s/%d.%s", importer.pcapOutputPath, job.Id, sanitize.Name(job.FileExtension)) - os.Remove(pcapOutputFilepath) + pcapOutputFilepath := fmt.Sprintf("%s/%d.%s", importer.pcapOutputPath, job.Id, sanitize.Name(job.FileExtension)) + os.Remove(pcapOutputFilepath) } func (importer *Importer) GetDataEpoch() time.Time { - // Epoch not used for imported data, return current time - return time.Now() + // Epoch not used for imported data, return current time + return time.Now() } func (importer *Importer) buildQuery(job *model.Job) string { - query := "" - - if len(job.Filter.SrcIp) > 0 { - if len(query) > 0 { - query = query + " and" - } - query = fmt.Sprintf("%s host %s", query, job.Filter.SrcIp) - } - - if len(job.Filter.DstIp) > 0 { - if len(query) > 0 { - query = query + " and" - } - query = fmt.Sprintf("%s host %s", query, job.Filter.DstIp) - } - - if job.Filter.SrcPort > 0 { - if len(query) > 0 { - query = query + " and" - } - query = fmt.Sprintf("%s port %d", query, job.Filter.SrcPort) - } - - if job.Filter.DstPort > 0 { - if len(query) > 0 { - query = query + " and" - } - query = fmt.Sprintf("%s port %d", query, job.Filter.DstPort) - } - - return query -} \ No newline at end of file + query := "" + + if len(job.Filter.SrcIp) > 0 { + if len(query) > 0 { + query = query + " and" + } + query = fmt.Sprintf("%s host %s", query, job.Filter.SrcIp) + } + + if len(job.Filter.DstIp) > 0 { + if len(query) > 0 { + query = query + " and" + } + query = fmt.Sprintf("%s host %s", query, job.Filter.DstIp) + } + + if job.Filter.SrcPort > 0 { + if len(query) > 0 { + query = query + " and" + } + query = fmt.Sprintf("%s port %d", query, job.Filter.SrcPort) + } + + if job.Filter.DstPort > 0 { + if len(query) > 0 { + query = query + " and" + } + query = fmt.Sprintf("%s port %d", query, job.Filter.DstPort) + } + + return query +} diff --git a/agent/modules/importer/importer_test.go b/agent/modules/importer/importer_test.go index f781005a..6f436984 100644 --- a/agent/modules/importer/importer_test.go +++ b/agent/modules/importer/importer_test.go @@ -10,68 +10,56 @@ package importer import ( - "testing" - "time" - "github.com/security-onion-solutions/securityonion-soc/model" + "testing" + "time" + + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/stretchr/testify/assert" ) func TestInitImporter(tester *testing.T) { - cfg := make(map[string]interface{}) - sq := NewImporter(nil) - err := sq.Init(cfg) - if err == nil { - tester.Errorf("expected non-nil error during init") - } - if sq.executablePath != DEFAULT_EXECUTABLE_PATH { - tester.Errorf("expected executablePath of %s but got %s", DEFAULT_EXECUTABLE_PATH, sq.executablePath) - } - if sq.pcapOutputPath != DEFAULT_PCAP_OUTPUT_PATH { - tester.Errorf("expected pcapOutputPath of %s but got %s", DEFAULT_PCAP_OUTPUT_PATH, sq.pcapOutputPath) - } - if sq.pcapInputPath != DEFAULT_PCAP_INPUT_PATH { - tester.Errorf("expected pcapInputPath of %s but got %s", DEFAULT_PCAP_INPUT_PATH, sq.pcapInputPath) - } - if sq.timeoutMs != DEFAULT_TIMEOUT_MS { - tester.Errorf("expected timeoutMs of %d but got %d", DEFAULT_TIMEOUT_MS, sq.timeoutMs) - } + cfg := make(map[string]interface{}) + sq := NewImporter(nil) + err := sq.Init(cfg) + assert.NotNil(tester, err) + assert.Equal(tester, DEFAULT_EXECUTABLE_PATH, sq.executablePath) + assert.Equal(tester, DEFAULT_PCAP_OUTPUT_PATH, sq.pcapOutputPath) + assert.Equal(tester, DEFAULT_PCAP_INPUT_PATH, sq.pcapInputPath) + assert.Equal(tester, DEFAULT_TIMEOUT_MS, sq.timeoutMs) } func TestDataLag(tester *testing.T) { - cfg := make(map[string]interface{}) - sq := NewImporter(nil) - sq.Init(cfg) - epoch := sq.GetDataEpoch() - if epoch.After(time.Now()) { - tester.Errorf("expected epoch date to be before or equal to current date") - } + cfg := make(map[string]interface{}) + sq := NewImporter(nil) + sq.Init(cfg) + epoch := sq.GetDataEpoch() + assert.False(tester, epoch.After(time.Now()), "epoch datetime should be before or equal to current datetime") } func validateQuery(tester *testing.T, actual string, expected string) { - if actual != expected { - tester.Errorf("expected '%s' but got '%s'", expected, actual) - } + assert.Equal(tester, expected, actual) } func TestBuildQuery(tester *testing.T) { - importer := NewImporter(nil) - job := model.NewJob() + importer := NewImporter(nil) + job := model.NewJob() - query := importer.buildQuery(job) - validateQuery(tester, query, "") + query := importer.buildQuery(job) + validateQuery(tester, query, "") - job.Filter.SrcIp = "1.2.3.4" - query = importer.buildQuery(job) - validateQuery(tester, query, " host 1.2.3.4") + job.Filter.SrcIp = "1.2.3.4" + query = importer.buildQuery(job) + validateQuery(tester, query, " host 1.2.3.4") - job.Filter.DstIp = "4.3.2.1" - query = importer.buildQuery(job) - validateQuery(tester, query, " host 1.2.3.4 and host 4.3.2.1") + job.Filter.DstIp = "4.3.2.1" + query = importer.buildQuery(job) + validateQuery(tester, query, " host 1.2.3.4 and host 4.3.2.1") - job.Filter.DstPort = 53 - query = importer.buildQuery(job) - validateQuery(tester, query, " host 1.2.3.4 and host 4.3.2.1 and port 53") + job.Filter.DstPort = 53 + query = importer.buildQuery(job) + validateQuery(tester, query, " host 1.2.3.4 and host 4.3.2.1 and port 53") - job.Filter.SrcPort = 33 - query = importer.buildQuery(job) - validateQuery(tester, query, " host 1.2.3.4 and host 4.3.2.1 and port 33 and port 53") -} \ No newline at end of file + job.Filter.SrcPort = 33 + query = importer.buildQuery(job) + validateQuery(tester, query, " host 1.2.3.4 and host 4.3.2.1 and port 33 and port 53") +} diff --git a/agent/modules/modules_test.go b/agent/modules/modules_test.go index 5c2ad81b..ee833cb3 100644 --- a/agent/modules/modules_test.go +++ b/agent/modules/modules_test.go @@ -10,19 +10,20 @@ package modules import ( - "testing" - "github.com/security-onion-solutions/securityonion-soc/module" + "testing" + + "github.com/security-onion-solutions/securityonion-soc/module" ) func TestBuildModuleMap(tester *testing.T) { - mm := BuildModuleMap(nil) - findModule(tester, mm, "importer") - findModule(tester, mm, "statickeyauth") - findModule(tester, mm, "stenoquery") + mm := BuildModuleMap(nil) + findModule(tester, mm, "importer") + findModule(tester, mm, "statickeyauth") + findModule(tester, mm, "stenoquery") } func findModule(tester *testing.T, mm map[string]module.Module, module string) { - if _, ok := mm[module]; !ok { - tester.Errorf("missing module %s", module) - } + if _, ok := mm[module]; !ok { + tester.Errorf("missing module %s", module) + } } diff --git a/agent/modules/statickeyauth/statickeyauth_test.go b/agent/modules/statickeyauth/statickeyauth_test.go index d2f13774..26ec6e6a 100644 --- a/agent/modules/statickeyauth/statickeyauth_test.go +++ b/agent/modules/statickeyauth/statickeyauth_test.go @@ -10,23 +10,19 @@ package statickeyauth import ( - "testing" + "testing" + + "github.com/stretchr/testify/assert" ) func TestInitStaticKeyAuth(tester *testing.T) { - cfg := make(map[string]interface{}) - auth := NewStaticKeyAuth(nil) - err := auth.Init(cfg) - if err == nil { - tester.Error("expected missing apiKey error") - } + cfg := make(map[string]interface{}) + auth := NewStaticKeyAuth(nil) + err := auth.Init(cfg) + assert.Error(tester, err) - cfg["apiKey"] = "123" - err = auth.Init(cfg) - if auth.apiKey != "123" { - tester.Errorf("expected apiKey %s but got %s", cfg["apiKey"], auth.apiKey) - } - if err == nil { - tester.Error("expected missing apiKey error") - } + cfg["apiKey"] = "123" + err = auth.Init(cfg) + assert.Error(tester, err) + assert.Equal(tester, "123", auth.apiKey) } diff --git a/agent/modules/stenoquery/stenoquery_test.go b/agent/modules/stenoquery/stenoquery_test.go index e8fa24f8..be5a19b1 100644 --- a/agent/modules/stenoquery/stenoquery_test.go +++ b/agent/modules/stenoquery/stenoquery_test.go @@ -15,33 +15,20 @@ import ( "time" "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/stretchr/testify/assert" ) func TestInitStenoQuery(tester *testing.T) { cfg := make(map[string]interface{}) sq := NewStenoQuery(nil) err := sq.Init(cfg) - if err == nil { - tester.Errorf("expected non-nil error during init") - } - if sq.executablePath != DEFAULT_EXECUTABLE_PATH { - tester.Errorf("expected executablePath of %s but got %s", DEFAULT_EXECUTABLE_PATH, sq.executablePath) - } - if sq.pcapOutputPath != DEFAULT_PCAP_OUTPUT_PATH { - tester.Errorf("expected pcapOutputPath of %s but got %s", DEFAULT_PCAP_OUTPUT_PATH, sq.pcapOutputPath) - } - if sq.pcapInputPath != DEFAULT_PCAP_INPUT_PATH { - tester.Errorf("expected pcapInputPath of %s but got %s", DEFAULT_PCAP_INPUT_PATH, sq.pcapInputPath) - } - if sq.epochRefreshMs != DEFAULT_EPOCH_REFRESH_MS { - tester.Errorf("expected epochRefreshMs of %d but got %d", DEFAULT_EPOCH_REFRESH_MS, sq.epochRefreshMs) - } - if sq.timeoutMs != DEFAULT_TIMEOUT_MS { - tester.Errorf("expected timeoutMs of %d but got %d", DEFAULT_TIMEOUT_MS, sq.timeoutMs) - } - if sq.dataLagMs != DEFAULT_DATA_LAG_MS { - tester.Errorf("expected dataLagMs of %d but got %d", DEFAULT_DATA_LAG_MS, sq.dataLagMs) - } + assert.Error(tester, err) + assert.Equal(tester, DEFAULT_EXECUTABLE_PATH, sq.executablePath) + assert.Equal(tester, DEFAULT_PCAP_OUTPUT_PATH, sq.pcapOutputPath) + assert.Equal(tester, DEFAULT_PCAP_INPUT_PATH, sq.pcapInputPath) + assert.Equal(tester, DEFAULT_TIMEOUT_MS, sq.timeoutMs) + assert.Equal(tester, DEFAULT_EPOCH_REFRESH_MS, sq.epochRefreshMs) + assert.Equal(tester, DEFAULT_DATA_LAG_MS, sq.dataLagMs) } func TestDataLag(tester *testing.T) { @@ -49,9 +36,7 @@ func TestDataLag(tester *testing.T) { sq := NewStenoQuery(nil) sq.Init(cfg) lagDate := sq.getDataLagDate() - if lagDate.After(time.Now()) { - tester.Errorf("expected data lag date to be before current date") - } + assert.False(tester, lagDate.After(time.Now()), "expected data lag datetime to be before current datetime") } func TestCreateQuery(tester *testing.T) { @@ -62,35 +47,25 @@ func TestCreateQuery(tester *testing.T) { job.Filter.EndTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:06:05Z") expectedQuery := "before 2006-01-02T15:06:05Z and after 2006-01-02T15:05:05Z" query := sq.CreateQuery(job) - if query != expectedQuery { - tester.Errorf("expected query %s to equal %s", query, expectedQuery) - } + assert.Equal(tester, expectedQuery, query) job.Filter.SrcIp = "1.2.3.4" query = sq.CreateQuery(job) expectedQuery = expectedQuery + " and host " + job.Filter.SrcIp - if query != expectedQuery { - tester.Errorf("expected query %s to equal %s", query, expectedQuery) - } + assert.Equal(tester, expectedQuery, query) job.Filter.DstIp = "1.2.1.2" query = sq.CreateQuery(job) expectedQuery = expectedQuery + " and host " + job.Filter.DstIp - if query != expectedQuery { - tester.Errorf("expected query %s to equal %s", query, expectedQuery) - } + assert.Equal(tester, expectedQuery, query) job.Filter.SrcPort = 123 query = sq.CreateQuery(job) expectedQuery = expectedQuery + " and port " + strconv.Itoa(job.Filter.SrcPort) - if query != expectedQuery { - tester.Errorf("expected query %s to equal %s", query, expectedQuery) - } + assert.Equal(tester, expectedQuery, query) job.Filter.DstPort = 123 query = sq.CreateQuery(job) expectedQuery = expectedQuery + " and port " + strconv.Itoa(job.Filter.DstPort) - if query != expectedQuery { - tester.Errorf("expected query %s to equal %s", query, expectedQuery) - } + assert.Equal(tester, expectedQuery, query) } diff --git a/cmd/sensoroni_test.go b/cmd/sensoroni_test.go index d846a335..cbd34a50 100644 --- a/cmd/sensoroni_test.go +++ b/cmd/sensoroni_test.go @@ -11,18 +11,17 @@ package main import ( - "os" - "testing" + "os" + "testing" + + "github.com/stretchr/testify/assert" ) func TestInitLogging(tester *testing.T) { - testFile := "/tmp/sensoroni_test.log" - defer os.Remove(testFile) - file, err := InitLogging(testFile, "debug") - if err != nil { - tester.Errorf("expected no errors") - } - if file == nil { - tester.Errorf("expected non-nil log file") - } + testFile := "/tmp/sensoroni_test.log" + defer os.Remove(testFile) + file, err := InitLogging(testFile, "debug") + if assert.Nil(tester, err) { + assert.NotNil(tester, file) + } } diff --git a/config/agentconfig_test.go b/config/agentconfig_test.go index 61dcb7c3..9e1a2e88 100644 --- a/config/agentconfig_test.go +++ b/config/agentconfig_test.go @@ -11,35 +11,25 @@ package config import ( - "testing" + "testing" + + "github.com/stretchr/testify/assert" ) func TestVerifyAgent(tester *testing.T) { - cfg := &AgentConfig{} - err := cfg.Verify() - if cfg.PollIntervalMs != DEFAULT_POLL_INTERVAL_MS { - tester.Errorf("expected PollIntervalMs %d but got %d", DEFAULT_POLL_INTERVAL_MS, cfg.PollIntervalMs) - } - if cfg.NodeId == "" { - tester.Errorf("expected non-empty NodeId") - } - if cfg.Model != "" { - tester.Errorf("expected blank model by default") - } - if cfg.VerifyCert == true { - tester.Errorf("expected VerifyCert to be false") - } - if err == nil { - tester.Errorf("expected ServerUrl error") - } + cfg := &AgentConfig{} + err := cfg.Verify() + assert.Equal(tester, DEFAULT_POLL_INTERVAL_MS, cfg.PollIntervalMs) + assert.NotEmpty(tester, cfg.NodeId) + assert.Empty(tester, cfg.Model) + assert.False(tester, cfg.VerifyCert) + assert.Error(tester, err) + + cfg.PollIntervalMs = 123 + cfg.ServerUrl = "http://some.where" + err = cfg.Verify() - cfg.PollIntervalMs = 123 - cfg.ServerUrl = "http://some.where" - err = cfg.Verify() - if cfg.PollIntervalMs != 123 { - tester.Errorf("expected PollIntervalMs %d but got %d", 123, cfg.PollIntervalMs) - } - if err != nil { - tester.Errorf("expected no error") - } + if assert.Nil(tester, err) { + assert.Equal(tester, 123, cfg.PollIntervalMs) + } } diff --git a/config/clientparameters_test.go b/config/clientparameters_test.go index d971b6c8..bf88961e 100644 --- a/config/clientparameters_test.go +++ b/config/clientparameters_test.go @@ -11,91 +11,63 @@ package config import ( - "testing" + "testing" + + "github.com/stretchr/testify/assert" ) func TestVerifyClientParameters(tester *testing.T) { - params := &ClientParameters{} + params := &ClientParameters{} err := params.Verify() - if err != nil { - tester.Errorf("expected no error") - } - if params.WebSocketTimeoutMs != 0 { - tester.Errorf("expected 0 but got %d", params.WebSocketTimeoutMs) - } - if params.TipTimeoutMs != 0 { - tester.Errorf("expected 0 but got %d", params.TipTimeoutMs) - } - if params.ApiTimeoutMs != 0 { - tester.Errorf("expected 0 but got %d", params.ApiTimeoutMs) - } - if params.CacheExpirationMs != 0 { - tester.Errorf("expected 0 but got %d", params.CacheExpirationMs) - } + if assert.Nil(tester, err) { + assert.Zero(tester, params.WebSocketTimeoutMs) + assert.Zero(tester, params.TipTimeoutMs) + assert.Zero(tester, params.ApiTimeoutMs) + assert.Zero(tester, params.CacheExpirationMs) + } } func TestVerifyHuntingParams(tester *testing.T) { params := &HuntingParameters{} err := params.Verify() - if err != nil { - tester.Errorf("expected no error") - } - if params.GroupFetchLimit != DEFAULT_GROUP_FETCH_LIMIT { - tester.Errorf("expected GroupFetchLimit %d but got %d", DEFAULT_GROUP_FETCH_LIMIT, params.GroupFetchLimit) - } - if params.EventFetchLimit != DEFAULT_EVENT_FETCH_LIMIT { - tester.Errorf("expected EventFetchLimit %d but got %d", DEFAULT_EVENT_FETCH_LIMIT, params.EventFetchLimit) - } - if params.RelativeTimeValue != DEFAULT_RELATIVE_TIME_VALUE { - tester.Errorf("expected RelativeTimeValue %d but got %d", DEFAULT_RELATIVE_TIME_VALUE, params.RelativeTimeValue) - } - if params.RelativeTimeUnit != DEFAULT_RELATIVE_TIME_UNIT { - tester.Errorf("expected RelativeTimeUnit %d but got %d", DEFAULT_RELATIVE_TIME_UNIT, params.RelativeTimeUnit) - } + if assert.Nil(tester, err) { + assert.Equal(tester, DEFAULT_GROUP_FETCH_LIMIT, params.GroupFetchLimit) + assert.Equal(tester, DEFAULT_EVENT_FETCH_LIMIT, params.EventFetchLimit) + assert.Equal(tester, DEFAULT_RELATIVE_TIME_VALUE, params.RelativeTimeValue) + assert.Equal(tester, DEFAULT_RELATIVE_TIME_UNIT, params.RelativeTimeUnit) + } } func TestCombineEmptyDeprecatedLinkIntoEmptyLinks(tester *testing.T) { - action := &HuntingAction{} - params := &HuntingParameters{} - params.Actions = append(params.Actions, action) - params.combineDeprecatedLinkIntoLinks() - if len(action.Links) != 0 { - tester.Errorf("expected empty links list but got %d", len(action.Links)) - } + action := &HuntingAction{} + params := &HuntingParameters{} + params.Actions = append(params.Actions, action) + params.combineDeprecatedLinkIntoLinks() + assert.Len(tester, action.Links, 0) } func TestCombineDeprecatedLinkIntoEmptyLinks(tester *testing.T) { - action := &HuntingAction{} - params := &HuntingParameters{} - params.Actions = append(params.Actions, action) - params.combineDeprecatedLinkIntoLinks() - if len(action.Links) != 0 { - tester.Errorf("expected empty links list but got %d", len(action.Links)) - } + action := &HuntingAction{} + params := &HuntingParameters{} + params.Actions = append(params.Actions, action) + params.combineDeprecatedLinkIntoLinks() + assert.Len(tester, action.Links, 0) - action.Link = "test" - params.combineDeprecatedLinkIntoLinks() - if len(action.Links) != 1 { - tester.Errorf("expected single item in links list but got %d", len(action.Links)) - } - if len(action.Link) != 0 { - tester.Errorf("expected empty link but got %d", len(action.Link)) - } + action.Link = "test" + params.combineDeprecatedLinkIntoLinks() + assert.Len(tester, action.Links, 1) + assert.Len(tester, action.Link, 0) } func TestCombineDeprecatedLinkIntoNonEmptyLinks(tester *testing.T) { - action := &HuntingAction{} - params := &HuntingParameters{} - params.Actions = append(params.Actions, action) - params.combineDeprecatedLinkIntoLinks() + action := &HuntingAction{} + params := &HuntingParameters{} + params.Actions = append(params.Actions, action) + params.combineDeprecatedLinkIntoLinks() - action.Link = "test" - action.Links = append(action.Links, "new-item") - params.combineDeprecatedLinkIntoLinks() - if len(action.Links) != 2 { - tester.Errorf("expected 2 items in links list but got %d", len(action.Links)) - } - if len(action.Link) != 0 { - tester.Errorf("expected empty link but got %d", len(action.Link)) - } -} \ No newline at end of file + action.Link = "test" + action.Links = append(action.Links, "new-item") + params.combineDeprecatedLinkIntoLinks() + assert.Len(tester, action.Links, 2) + assert.Len(tester, action.Link, 0) +} diff --git a/config/serverconfig_test.go b/config/serverconfig_test.go index 44b04178..183e8d36 100644 --- a/config/serverconfig_test.go +++ b/config/serverconfig_test.go @@ -11,32 +11,24 @@ package config import ( - "testing" + "testing" + + "github.com/stretchr/testify/assert" ) func TestVerifyServer(tester *testing.T) { - cfg := &ServerConfig{} - err := cfg.Verify() - if cfg.MaxPacketCount != DEFAULT_MAX_PACKET_COUNT { - tester.Errorf("expected MaxPacketCount %d but got %d", DEFAULT_MAX_PACKET_COUNT, cfg.MaxPacketCount) - } - if cfg.IdleConnectionTimeoutMs != DEFAULT_IDLE_CONNECTION_TIMEOUT_MS { - tester.Errorf("expected IdleConnectionTimeoutMs %d but got %d", DEFAULT_IDLE_CONNECTION_TIMEOUT_MS, cfg.IdleConnectionTimeoutMs) - } - if err == nil { - tester.Errorf("expected bind address error") - } + cfg := &ServerConfig{} + err := cfg.Verify() + if assert.Error(tester, err) { + assert.Equal(tester, DEFAULT_MAX_PACKET_COUNT, cfg.MaxPacketCount) + assert.Equal(tester, DEFAULT_IDLE_CONNECTION_TIMEOUT_MS, cfg.IdleConnectionTimeoutMs) + } - cfg.BindAddress = "http://some.where" - cfg.MaxPacketCount = 123 - err = cfg.Verify() - if cfg.MaxPacketCount != 123 { - tester.Errorf("expected PollIntervalMs %d but got %d", 123, cfg.MaxPacketCount) - } - if err != nil { - tester.Errorf("expected no error") - } - if cfg.TimezoneScript != "/opt/sensoroni/scripts/timezones.sh" { - tester.Errorf("Unexpected default timezone script: %s", cfg.TimezoneScript) - } + cfg.BindAddress = "http://some.where" + cfg.MaxPacketCount = 123 + err = cfg.Verify() + if assert.Nil(tester, err) { + assert.Equal(tester, 123, cfg.MaxPacketCount) + assert.Equal(tester, "/opt/sensoroni/scripts/timezones.sh", cfg.TimezoneScript) + } } diff --git a/json/json_test.go b/json/json_test.go index 7ce62eef..5a1231e5 100644 --- a/json/json_test.go +++ b/json/json_test.go @@ -11,25 +11,22 @@ package json import ( - "os" - "testing" + "os" + "testing" + + "github.com/stretchr/testify/assert" ) func TestJson(tester *testing.T) { - testFile := "/tmp/sensoroni_test.json" - defer os.Remove(testFile) - obj := make(map[string]string) - obj["MyKey"] = "MyValue" - err := WriteJsonFile(testFile, obj) - if err != nil { - tester.Errorf("unexpected write error") - } - obj = make(map[string]string) - err = LoadJsonFile(testFile, &obj) - if err != nil { - tester.Errorf("unexpected load error") - } - if obj["MyKey"] != "MyValue" { - tester.Errorf("expected value %s but got %s", "MyValue", obj["MyKey"]) - } + testFile := "/tmp/sensoroni_test.json" + defer os.Remove(testFile) + obj := make(map[string]string) + obj["MyKey"] = "MyValue" + err := WriteJsonFile(testFile, obj) + assert.Nil(tester, err) + obj = make(map[string]string) + err = LoadJsonFile(testFile, &obj) + if assert.Nil(tester, err) { + assert.Equal(tester, "MyValue", obj["MyKey"]) + } } diff --git a/model/event_test.go b/model/event_test.go index a6437c69..d5fce96b 100644 --- a/model/event_test.go +++ b/model/event_test.go @@ -11,67 +11,68 @@ package model import ( - "github.com/stretchr/testify/assert" - "testing" - "time" + "testing" + "time" + + "github.com/stretchr/testify/assert" ) func TestNewEventSearchCriteria(tester *testing.T) { - event := NewEventSearchCriteria() - assert.NotZero(tester, event.CreateTime) + event := NewEventSearchCriteria() + assert.NotZero(tester, event.CreateTime) } func TestNewEventUpdateCriteria(tester *testing.T) { - event := NewEventUpdateCriteria() - assert.NotZero(tester, event.CreateTime) + event := NewEventUpdateCriteria() + assert.NotZero(tester, event.CreateTime) } func TestPopulateQueryTrim(tester *testing.T) { - goodTime := "2006-05-07T14:15:59+01:00 - 2006-05-07T14:16:59+01:00" - zone := "America/New_York" - criteria := NewEventSearchCriteria() - _ = criteria.Populate(" foo ", goodTime, time.RFC3339, zone, "10", "100") - assert.Equal(tester, criteria.RawQuery, "foo") + goodTime := "2006-05-07T14:15:59+01:00 - 2006-05-07T14:16:59+01:00" + zone := "America/New_York" + criteria := NewEventSearchCriteria() + _ = criteria.Populate(" foo ", goodTime, time.RFC3339, zone, "10", "100") + assert.Equal(tester, "foo", criteria.RawQuery) } func TestPopulateBadInputTimes(tester *testing.T) { - badTime := "2006-05-07" - goodTime := "2006-05-07T14:15:59+01:00" - zone := "America/New_York" - criteria := NewEventSearchCriteria() - err := criteria.Populate("foo", badTime+" - "+badTime, time.RFC3339, zone, "10", "100") - assert.Error(tester, err, "expected error from bad start time and end time input") + badTime := "2006-05-07" + goodTime := "2006-05-07T14:15:59+01:00" + zone := "America/New_York" + criteria := NewEventSearchCriteria() + err := criteria.Populate("foo", badTime+" - "+badTime, time.RFC3339, zone, "10", "100") + assert.Error(tester, err, "expected error from bad start time and end time input") - err = criteria.Populate("foo", badTime+" - "+goodTime, time.RFC3339, zone, "10", "100") - assert.Error(tester, err, "expected error from bad start time input") + err = criteria.Populate("foo", badTime+" - "+goodTime, time.RFC3339, zone, "10", "100") + assert.Error(tester, err, "expected error from bad start time input") - err = criteria.Populate("foo", goodTime+" - "+badTime, time.RFC3339, zone, "10", "100") - assert.Error(tester, err, "expected error from bad end time input") + err = criteria.Populate("foo", goodTime+" - "+badTime, time.RFC3339, zone, "10", "100") + assert.Error(tester, err, "expected error from bad end time input") - err = criteria.Populate("foo", goodTime+" - "+goodTime, time.RFC3339, zone, "30", "100") - assert.NoError(tester, err, "expected no error from good time input") + err = criteria.Populate("foo", goodTime+" - "+goodTime, time.RFC3339, zone, "30", "100") + assert.NoError(tester, err, "expected no error from good time input") } func TestLimits(tester *testing.T) { - goodTime := "2006-05-07T14:15:59+01:00" - criteria := NewEventSearchCriteria() - _ = criteria.Populate("foo", goodTime+" - "+goodTime, time.RFC3339, "PST", "30", "100") - assert.Equal(tester, criteria.EventLimit, 100) - assert.Equal(tester, criteria.MetricLimit, 30) + goodTime := "2006-05-07T14:15:59+01:00" + criteria := NewEventSearchCriteria() + _ = criteria.Populate("foo", goodTime+" - "+goodTime, time.RFC3339, "PST", "30", "100") + assert.Equal(tester, 100, criteria.EventLimit) + assert.Equal(tester, 30, criteria.MetricLimit) } func TestNewEventSearchResult(tester *testing.T) { - event := NewEventSearchResults() - time.Sleep(1) - event.Complete() - assert.True(tester, event.CompleteTime.After(event.CreateTime), "expected CompleteTime to be newer than CreateTime") - assert.Len(tester, event.Errors, 0) + event := NewEventSearchResults() + time.Sleep(1 * time.Nanosecond) + event.Complete() + assert.True(tester, event.CompleteTime.After(event.CreateTime), "expected CompleteTime to be newer than CreateTime") + assert.Len(tester, event.Errors, 0) } func TestNewEventUpdateResult(tester *testing.T) { - event := NewEventUpdateResults() - time.Sleep(1) - event.Complete() - assert.True(tester, event.CompleteTime.After(event.CreateTime), "expected CompleteTime to be newer than CreateTime") - assert.Len(tester, event.Errors, 0) + event := NewEventUpdateResults() + time.Sleep(1 * time.Nanosecond) + event.Complete() + assert.True(tester, event.CompleteTime.After(event.CreateTime), "expected CompleteTime to be newer than CreateTime") + assert.Len(tester, event.Errors, 0) } diff --git a/model/job_test.go b/model/job_test.go index 87ceff24..cd941018 100644 --- a/model/job_test.go +++ b/model/job_test.go @@ -11,94 +11,58 @@ package model import ( - "errors" - "testing" + "errors" + "testing" + + "github.com/stretchr/testify/assert" ) func TestVerifyJob(tester *testing.T) { - job := NewJob() - if job.Status != JobStatusPending { - tester.Errorf("expected Status %d but got %d", JobStatusPending, job.Status) - } + job := NewJob() + assert.Equal(tester, JobStatusPending, job.Status) + + job.Fail(errors.New("one")) + assert.Equal(tester, JobStatusIncomplete, job.Status) + assert.NotEmpty(tester, job.Failure) + assert.Equal(tester, 1, job.FailCount) - job.Fail(errors.New("one")) - if job.Status != JobStatusIncomplete { - tester.Errorf("expected Status %d but got %d", JobStatusIncomplete, job.Status) - } - if job.Failure == "" { - tester.Errorf("expected Failure but got none") - } - if job.FailCount != 1 { - tester.Errorf("expected FailCount %d but got %d", 1, job.FailCount) - } - job.Fail(errors.New("two")) - if job.FailCount != 2 { - tester.Errorf("expected FailCount %d but got %d", 2, job.FailCount) - } + job.Fail(errors.New("two")) + assert.Equal(tester, 2, job.FailCount) - job.Complete() - if job.Status != JobStatusCompleted { - tester.Errorf("expected Status %d but got %d", JobStatusCompleted, job.Status) - } + job.Complete() + assert.Equal(tester, JobStatusCompleted, job.Status) } func TestSetNodeId(tester *testing.T) { - job := NewJob() - if job.NodeId != "" { - tester.Errorf("expected new jobs to have an empty node ID") - } + job := NewJob() + assert.Empty(tester, job.NodeId) - job.NodeId = "test" - if job.NodeId != "test" { - tester.Errorf("expected unmodified Node ID but got %s", job.NodeId) - } + job.SetNodeId("testing") + assert.Equal(tester, "testing", job.NodeId) + assert.Equal(tester, "testing", job.GetNodeId()) - job.SetNodeId("testing") - if job.NodeId != "testing" { - tester.Errorf("expected unmodified Node ID but got %s", job.NodeId) - } - if job.GetNodeId() != "testing" { - tester.Errorf("expected unmodified Node ID via getter but got %s", job.GetNodeId()) - } + job.SetNodeId("TestingThis") + assert.Equal(tester, "testingthis", job.NodeId) + assert.Equal(tester, "testingthis", job.GetNodeId()) - job.SetNodeId("TestingThis") - if job.NodeId != "testingthis" { - tester.Errorf("expected lowercased Node ID but got %s", job.NodeId) - } - if job.GetNodeId() != "testingthis" { - tester.Errorf("expected lowercased Node ID via getter but got %s", job.GetNodeId()) - } - - job.NodeId = "TestingThis2" - if job.NodeId != "TestingThis2" { - tester.Errorf("expected unmodified Node ID but got %s", job.NodeId) - } - if job.GetNodeId() != "testingthis2" { - tester.Errorf("expected lowercased Node ID via getter but got %s", job.GetNodeId()) - } - if job.NodeId != "testingthis2" { - tester.Errorf("expected lowercased Node ID after getter but got %s", job.NodeId) - } + // Check that NodeId is modified by getter + job.NodeId = "TestingThis2" + assert.Equal(tester, "TestingThis2", job.NodeId) + assert.Equal(tester, "testingthis2", job.GetNodeId()) + assert.Equal(tester, "testingthis2", job.NodeId) } func TestGetLegacyNodeId(tester *testing.T) { - job := NewJob() - if job.GetNodeId() != "" { - tester.Errorf("expected new jobs to have an empty node ID") - } + job := NewJob() + assert.Empty(tester, job.GetNodeId()) - job.NodeId = "Foo" - if job.GetNodeId() != "foo" { - tester.Errorf("expected foo but got %s", job.GetNodeId()) - } + job.NodeId = "Foo" + assert.Equal(tester, "foo", job.GetNodeId()) - job.LegacySensorId = "Bar" - if job.GetNodeId() != "foo" { - tester.Errorf("expected foo but got %s", job.GetNodeId()) - } + job.LegacySensorId = "Bar" + assert.Equal(tester, "foo", job.GetNodeId()) - job.NodeId = "" - if job.GetNodeId() != "bar" { - tester.Errorf("expected bar but got %s", job.GetNodeId()) - } -} \ No newline at end of file + // Check that GetNodeId() returns formatted LegacySensorId if NodeId is blank + job.NodeId = "" + assert.Equal(tester, "bar", job.GetNodeId()) +} diff --git a/model/node_test.go b/model/node_test.go index ac8e27c2..01de16f3 100644 --- a/model/node_test.go +++ b/model/node_test.go @@ -11,173 +11,164 @@ package model import ( - "testing" + "testing" + + "github.com/stretchr/testify/assert" ) func testModel(tester *testing.T, newModel string, model string, front string, back string) { - node := NewNode("") - node.SetModel(newModel) - - if node.Model != model { - tester.Errorf("Expected model %s but got %s", model, node.Model) - } - if node.ImageFront != front { - tester.Errorf("Expected front %s but got %s", front, node.ImageFront) - } - if node.ImageBack != back { - tester.Errorf("Expected back %s but got %s", back, node.ImageBack) - } + node := NewNode("") + node.SetModel(newModel) + assert.Equal(tester, model, node.Model) + assert.Equal(tester, front, node.ImageFront) + assert.Equal(tester, back, node.ImageBack) } func TestSetModel(tester *testing.T) { - testModel(tester, "", "N/A", "", ""); - testModel(tester, "foo", "N/A", "", ""); - testModel(tester, "SOSMN", "SOSMN", "sos-1u-front-thumb.jpg", "sos-1u-ethernet-back-thumb.jpg"); - testModel(tester, "SOS1000", "SOS1000", "sos-1u-front-thumb.jpg", "sos-1u-ethernet-back-thumb.jpg"); - testModel(tester, "SOS500", "SOS500", "sos-1u-front-thumb.jpg", "sos-1u-ethernet-back-thumb.jpg"); - testModel(tester, "SOSSNNV", "SOSSNNV", "sos-1u-front-thumb.jpg", "sos-1u-sfp-back-thumb.jpg"); - testModel(tester, "SOS1000F", "SOS1000F", "sos-1u-front-thumb.jpg", "sos-1u-sfp-back-thumb.jpg"); - testModel(tester, "SOS10K", "SOS10K", "sos-1u-front-thumb.jpg", "sos-1u-sfp-back-thumb.jpg"); - testModel(tester, "SOS4000", "SOS4000", "sos-2u-front-thumb.jpg", "sos-2u-back-thumb.jpg"); - testModel(tester, "SOSSN7200", "SOSSN7200", "sos-2u-front-thumb.jpg", "sos-2u-back-thumb.jpg"); - testModel(tester, "SO2AMI01", "SO2AMI01", "", ""); - testModel(tester, "SO2AZI01", "SO2AZI01", "", ""); - testModel(tester, "SO2GCI01", "SO2GCI01", "", ""); + testModel(tester, "", "N/A", "", "") + testModel(tester, "foo", "N/A", "", "") + testModel(tester, "SOSMN", "SOSMN", "sos-1u-front-thumb.jpg", "sos-1u-ethernet-back-thumb.jpg") + testModel(tester, "SOS1000", "SOS1000", "sos-1u-front-thumb.jpg", "sos-1u-ethernet-back-thumb.jpg") + testModel(tester, "SOS500", "SOS500", "sos-1u-front-thumb.jpg", "sos-1u-ethernet-back-thumb.jpg") + testModel(tester, "SOSSNNV", "SOSSNNV", "sos-1u-front-thumb.jpg", "sos-1u-sfp-back-thumb.jpg") + testModel(tester, "SOS1000F", "SOS1000F", "sos-1u-front-thumb.jpg", "sos-1u-sfp-back-thumb.jpg") + testModel(tester, "SOS10K", "SOS10K", "sos-1u-front-thumb.jpg", "sos-1u-sfp-back-thumb.jpg") + testModel(tester, "SOS4000", "SOS4000", "sos-2u-front-thumb.jpg", "sos-2u-back-thumb.jpg") + testModel(tester, "SOSSN7200", "SOSSN7200", "sos-2u-front-thumb.jpg", "sos-2u-back-thumb.jpg") + testModel(tester, "SO2AMI01", "SO2AMI01", "", "") + testModel(tester, "SO2AZI01", "SO2AZI01", "", "") + testModel(tester, "SO2GCI01", "SO2GCI01", "", "") } -func testStatus(tester *testing.T, - enhancedStatusEnabled bool, - nodeStatus string, - connectionStatus string, - raidStatus string, - processStatus string, - expectedStatus string) { - node := NewNode("") - node.Status = nodeStatus - node.ConnectionStatus = connectionStatus - node.RaidStatus = raidStatus - node.ProcessStatus = processStatus - result := node.UpdateOverallStatus(enhancedStatusEnabled) - shouldChange := nodeStatus != expectedStatus - if result != shouldChange { - tester.Errorf("Unexpected node status change") - } - if expectedStatus != node.Status { - tester.Errorf("Expected status %s but got %s [%s, %s, %s, %s]", expectedStatus, node.Status, nodeStatus, connectionStatus, raidStatus, processStatus) - } +func testStatus(tester *testing.T, + enhancedStatusEnabled bool, + nodeStatus string, + connectionStatus string, + raidStatus string, + processStatus string, + expectedStatus string) { + node := NewNode("") + node.Status = nodeStatus + node.ConnectionStatus = connectionStatus + node.RaidStatus = raidStatus + node.ProcessStatus = processStatus + result := node.UpdateOverallStatus(enhancedStatusEnabled) + shouldChange := nodeStatus != expectedStatus + assert.Equal(tester, shouldChange, result) + assert.Equal(tester, expectedStatus, node.Status) } 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, true, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, true, NodeStatusOk, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) - testStatus(tester, true, NodeStatusFault, NodeStatusUnknown, NodeStatusUnknown, NodeStatusUnknown, NodeStatusFault) + // If all component statuses are unknown then the node's overall status is fault, regardless of current status. + 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, 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) + // If only one status is not unknown then must be in fault state. + 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, 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) + // If only one status is not unknown then must be in fault state. + 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, 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) + // If an earlier component status is Ok then the subsequent status becomes the overall status, regardless of current status. + 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, 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, 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, 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 + // If an earlier component status is Fault then the subsequent status remains Fault, regardless of current status. + 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, 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, 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) +} diff --git a/model/query_test.go b/model/query_test.go index 77c4fc39..c166d9ea 100644 --- a/model/query_test.go +++ b/model/query_test.go @@ -10,154 +10,126 @@ package model import ( - "testing" + "testing" + + "github.com/stretchr/testify/assert" ) func validateQuery(tester *testing.T, args ...string) { - query := NewQuery() - err := query.Parse(args[0]) - expected := args[0] - if len(args) > 1 { - expected = args[1] - } - actual := query.String() - if err != nil { - actual = err.Error() - } - if actual != expected { - tester.Errorf("Expected [%s], but got [%s]", expected, actual) - } + query := NewQuery() + err := query.Parse(args[0]) + expected := args[0] + if len(args) > 1 { + expected = args[1] + } + actual := query.String() + if err != nil { + actual = err.Error() + } + assert.Equal(tester, expected, actual) } func TestQueries(tester *testing.T) { - validateQuery(tester, "abc") - validateQuery(tester, "abc def") - validateQuery(tester, "abc:'def'", "abc: 'def'") - validateQuery(tester, " abc0 def ", "abc0 def") - validateQuery(tester, "'abc1' def") - validateQuery(tester, "'abc2 def'") - validateQuery(tester, `"abc3' def"`) - - validateQuery(tester, "abc5,def", "abc5 def") - validateQuery(tester, "abc def | groupby jkl") - validateQuery(tester, "abc def | groupby 'jkl'") - validateQuery(tester, "'abc8 | groupby'") - validateQuery(tester, "abcA|", "abcA") - - validateQuery(tester, "(abc AND def)") - validateQuery(tester, "((abc AND def))") - validateQuery(tester, "((abc AND def:\"ghi\") AND (xyz=\"123\"))") - - validateQuery(tester, "abcA|groupby\njjj", "abcA | groupby jjj") - validateQuery(tester, "abcA|\ngroupby\tjjj", "abcA | groupby jjj") - - validateQuery(tester, "'abc4 def", "QUERY_INVALID__QUOTE_INCOMPLETE") - validateQuery(tester, "'abc9|", "QUERY_INVALID__QUOTE_INCOMPLETE") - - validateQuery(tester, "|", "QUERY_INVALID__SEGMENT_EMPTY") - validateQuery(tester, " |", "QUERY_INVALID__SEGMENT_EMPTY") - validateQuery(tester, " | abc", "QUERY_INVALID__SEGMENT_EMPTY") - validateQuery(tester, "abc6 def | |", "QUERY_INVALID__SEGMENT_EMPTY") - validateQuery(tester, "abc7 def || ", "QUERY_INVALID__SEGMENT_EMPTY") - - validateQuery(tester, "abc7 def ) ", "QUERY_INVALID__GROUP_NOT_STARTED") - validateQuery(tester, "abc7 def () ", "QUERY_INVALID__GROUP_EMPTY") - validateQuery(tester, "abc (d e f", "QUERY_INVALID__GROUP_INCOMPLETE") - validateQuery(tester, "abc (d e f | ghi 'jkl' | mno", "QUERY_INVALID__GROUP_INCOMPLETE") - - validateQuery(tester, "abc (d e f) | groupby 'jkl' | mno", "QUERY_INVALID__SEGMENT_UNSUPPORTED") - - validateQuery(tester, "", "QUERY_INVALID__SEARCH_MISSING") - validateQuery(tester, " ", "QUERY_INVALID__SEARCH_MISSING") - - validateQuery(tester, "abcA|groupby", "QUERY_INVALID__GROUPBY_TERMS_MISSING") - validateQuery(tester, "abcA|groupby ", "QUERY_INVALID__GROUPBY_TERMS_MISSING") + validateQuery(tester, "abc") + validateQuery(tester, "abc def") + validateQuery(tester, "abc:'def'", "abc: 'def'") + validateQuery(tester, " abc0 def ", "abc0 def") + validateQuery(tester, "'abc1' def") + validateQuery(tester, "'abc2 def'") + validateQuery(tester, `"abc3' def"`) + + validateQuery(tester, "abc5,def", "abc5 def") + validateQuery(tester, "abc def | groupby jkl") + validateQuery(tester, "abc def | groupby 'jkl'") + validateQuery(tester, "'abc8 | groupby'") + validateQuery(tester, "abcA|", "abcA") + + validateQuery(tester, "(abc AND def)") + validateQuery(tester, "((abc AND def))") + validateQuery(tester, "((abc AND def:\"ghi\") AND (xyz=\"123\"))") + + validateQuery(tester, "abcA|groupby\njjj", "abcA | groupby jjj") + validateQuery(tester, "abcA|\ngroupby\tjjj", "abcA | groupby jjj") + + validateQuery(tester, "'abc4 def", "QUERY_INVALID__QUOTE_INCOMPLETE") + validateQuery(tester, "'abc9|", "QUERY_INVALID__QUOTE_INCOMPLETE") + + validateQuery(tester, "|", "QUERY_INVALID__SEGMENT_EMPTY") + validateQuery(tester, " |", "QUERY_INVALID__SEGMENT_EMPTY") + validateQuery(tester, " | abc", "QUERY_INVALID__SEGMENT_EMPTY") + validateQuery(tester, "abc6 def | |", "QUERY_INVALID__SEGMENT_EMPTY") + validateQuery(tester, "abc7 def || ", "QUERY_INVALID__SEGMENT_EMPTY") + + validateQuery(tester, "abc7 def ) ", "QUERY_INVALID__GROUP_NOT_STARTED") + validateQuery(tester, "abc7 def () ", "QUERY_INVALID__GROUP_EMPTY") + validateQuery(tester, "abc (d e f", "QUERY_INVALID__GROUP_INCOMPLETE") + validateQuery(tester, "abc (d e f | ghi 'jkl' | mno", "QUERY_INVALID__GROUP_INCOMPLETE") + + validateQuery(tester, "abc (d e f) | groupby 'jkl' | mno", "QUERY_INVALID__SEGMENT_UNSUPPORTED") + + validateQuery(tester, "", "QUERY_INVALID__SEARCH_MISSING") + validateQuery(tester, " ", "QUERY_INVALID__SEARCH_MISSING") + + validateQuery(tester, "abcA|groupby", "QUERY_INVALID__GROUPBY_TERMS_MISSING") + validateQuery(tester, "abcA|groupby ", "QUERY_INVALID__GROUPBY_TERMS_MISSING") } func validateGroup(tester *testing.T, orig string, group string, expected string) { - query := NewQuery() - query.Parse(orig) - actual, err := query.Group(group) - if err != nil { - actual = err.Error() - } - if actual != expected { - tester.Errorf("Expected [%s], but got [%s]", expected, actual) - } + query := NewQuery() + query.Parse(orig) + actual, err := query.Group(group) + if err != nil { + actual = err.Error() + } + assert.Equal(tester, expected, actual) } func TestGroup(tester *testing.T) { - validateGroup(tester, "a", "b", "a | groupby b") - validateGroup(tester, "a|groupby b", "c", "a | groupby b c") - validateGroup(tester, "a|groupby b", "b", "a | groupby b") + validateGroup(tester, "a", "b", "a | groupby b") + validateGroup(tester, "a|groupby b", "c", "a | groupby b c") + validateGroup(tester, "a|groupby b", "b", "a | groupby b") } func validateFilter(tester *testing.T, orig string, key string, value string, scalar bool, mode string, expected string) { - query := NewQuery() - query.Parse(orig) - actual, err := query.Filter(key, value, scalar, mode) - if err != nil { - actual = err.Error() - } - if actual != expected { - tester.Errorf("Expected [%s], but got [%s]", expected, actual) - } + query := NewQuery() + query.Parse(orig) + actual, err := query.Filter(key, value, scalar, mode) + if err != nil { + actual = err.Error() + } + assert.Equal(tester, expected, actual) } func TestFilter(tester *testing.T) { - validateFilter(tester, "a", "b", "c", false, FILTER_INCLUDE, "a AND b:\"c\"") - validateFilter(tester, "a", "b", "c", false, FILTER_EXCLUDE, "a AND NOT b:\"c\"") - validateFilter(tester, "", "b", "c", false, FILTER_INCLUDE, "b:\"c\"") - validateFilter(tester, "", "b", "1", true, FILTER_INCLUDE, "b:1") - validateFilter(tester, "(a:1 OR c:2) | groupby z", "b", "1", true, FILTER_EXACT, "b:1 | groupby z") - validateFilter(tester, "(a:1 OR c:2) | groupby z", "b", "1", true, FILTER_DRILLDOWN, "(a:1 OR c:2) AND b:1") - validateFilter(tester, "a", "soc_b", "1", true, FILTER_INCLUDE, "a AND _b:1") - validateFilter(tester, "a:1", "a", "2", true, FILTER_INCLUDE, "a:1 AND a:2") - validateFilter(tester, "a: 1", "a", "2", true, FILTER_INCLUDE, "a: 1 AND a:2") - validateFilter(tester, "NOT a:1", "a", "2", true, FILTER_EXCLUDE, "NOT a:1 AND NOT a:2") + validateFilter(tester, "a", "b", "c", false, FILTER_INCLUDE, "a AND b:\"c\"") + validateFilter(tester, "a", "b", "c", false, FILTER_EXCLUDE, "a AND NOT b:\"c\"") + validateFilter(tester, "", "b", "c", false, FILTER_INCLUDE, "b:\"c\"") + validateFilter(tester, "", "b", "1", true, FILTER_INCLUDE, "b:1") + validateFilter(tester, "(a:1 OR c:2) | groupby z", "b", "1", true, FILTER_EXACT, "b:1 | groupby z") + validateFilter(tester, "(a:1 OR c:2) | groupby z", "b", "1", true, FILTER_DRILLDOWN, "(a:1 OR c:2) AND b:1") + validateFilter(tester, "a", "soc_b", "1", true, FILTER_INCLUDE, "a AND _b:1") + validateFilter(tester, "a:1", "a", "2", true, FILTER_INCLUDE, "a:1 AND a:2") + validateFilter(tester, "a: 1", "a", "2", true, FILTER_INCLUDE, "a: 1 AND a:2") + validateFilter(tester, "NOT a:1", "a", "2", true, FILTER_EXCLUDE, "NOT a:1 AND NOT a:2") } func TestIsScalar(tester *testing.T) { - if !IsScalar(1) { - tester.Errorf("Expected int to be scalar") - } - - if !IsScalar(false) { - tester.Errorf("Expected false to be scalar") - } - - if !IsScalar(true) { - tester.Errorf("Expected true to be scalar") - } - - if !IsScalar(22.1) { - tester.Errorf("Expected 22.1 to be scalar") - } - - if IsScalar("str") { - tester.Errorf("Expected 'str' to not be scalar") - } + assert.True(tester, IsScalar(1)) + assert.True(tester, IsScalar(false)) + assert.True(tester, IsScalar(true)) + assert.True(tester, IsScalar(22.1)) + assert.False(tester, IsScalar("str")) } func TestRemoveTermsWith(tester *testing.T) { - segment := NewSearchSegmentEmpty() - if segment.RemoveTermsWith("hello") != 0 { - tester.Errorf("Expected no terms removed on empty segment") - } - segment.AddFilter("hello", "a", false, false) - if segment.RemoveTermsWith("hello") != 1 { - tester.Errorf("Expected one term removed on single term segment") - } - if segment.RemoveTermsWith("hello") != 0 { - tester.Errorf("Expected no terms removed on already removed term") - } - segment.AddFilter("there", "b", false, false) - if segment.RemoveTermsWith("hello") != 0 { - tester.Errorf("Expected no terms removed on unmatched term") - } - segment.AddFilter("and", "c", false, false) - segment.AddFilter("goodbye", "d", false, false) - if segment.RemoveTermsWith("e") != 2 { - tester.Errorf("Expected two terms removed") - } -} \ No newline at end of file + segment := NewSearchSegmentEmpty() + assert.Zero(tester, segment.RemoveTermsWith("hello"), "Expected no terms removed on empty segment") + segment.AddFilter("hello", "a", false, false) + assert.Equal(tester, 1, segment.RemoveTermsWith("hello")) + assert.Zero(tester, segment.RemoveTermsWith("hello"), "Expected no terms removed on already removed term") + segment.AddFilter("there", "b", false, false) + assert.Zero(tester, segment.RemoveTermsWith("hello"), "Expected no terms removed on unmatched term") + segment.AddFilter("and", "c", false, false) + segment.AddFilter("goodbye", "d", false, false) + assert.Equal(tester, 2, segment.RemoveTermsWith("e")) +} diff --git a/model/unauthorized_test.go b/model/unauthorized_test.go index 6deb9edc..9b3b5407 100644 --- a/model/unauthorized_test.go +++ b/model/unauthorized_test.go @@ -11,14 +11,15 @@ package model import ( - "github.com/stretchr/testify/assert" - "testing" + "testing" + + "github.com/stretchr/testify/assert" ) func TestNewUnauthorized(tester *testing.T) { - event := NewUnauthorized("mysubject", "myop", "mytarget") - assert.NotZero(tester, event.CreateTime) - assert.Equal(tester, event.Subject, "mysubject") - assert.Equal(tester, event.Operation, "myop") - assert.Equal(tester, event.Target, "mytarget") + event := NewUnauthorized("mysubject", "myop", "mytarget") + assert.NotZero(tester, event.CreateTime) + assert.Equal(tester, "mysubject", event.Subject) + assert.Equal(tester, "myop", event.Operation) + assert.Equal(tester, "mytarget", event.Target) } diff --git a/module/manager_test.go b/module/manager_test.go index f4650146..29e5eab1 100644 --- a/module/manager_test.go +++ b/module/manager_test.go @@ -11,31 +11,27 @@ package module import ( - "testing" + "testing" + + "github.com/stretchr/testify/assert" ) func TestMeetsPrerequisites(tester *testing.T) { - mgr := NewModuleManager() - mcm := make(ModuleConfigMap) - - prereqs := make([]string, 0) - prereqs = append(prereqs, "foo") - prereqs = append(prereqs, "bar") - - actual := mgr.meetsPrerequisites(prereqs, mcm) - if actual != false { - tester.Errorf("expected meetsPrerequisites %t but got %t", false, actual) - } - - mcm["foo"] = make(ModuleConfig) - actual = mgr.meetsPrerequisites(prereqs, mcm) - if actual != false { - tester.Errorf("expected meetsPrerequisites %t but got %t", false, actual) - } - - mcm["bar"] = make(ModuleConfig) - actual = mgr.meetsPrerequisites(prereqs, mcm) - if actual != true { - tester.Errorf("expected meetsPrerequisites %t but got %t", true, actual) - } + mgr := NewModuleManager() + mcm := make(ModuleConfigMap) + + prereqs := make([]string, 0) + prereqs = append(prereqs, "foo") + prereqs = append(prereqs, "bar") + + actual := mgr.meetsPrerequisites(prereqs, mcm) + assert.False(tester, actual) + + mcm["foo"] = make(ModuleConfig) + actual = mgr.meetsPrerequisites(prereqs, mcm) + assert.False(tester, actual) + + mcm["bar"] = make(ModuleConfig) + actual = mgr.meetsPrerequisites(prereqs, mcm) + assert.True(tester, actual) } diff --git a/module/options_test.go b/module/options_test.go index d8a07a4f..aa141721 100644 --- a/module/options_test.go +++ b/module/options_test.go @@ -11,134 +11,99 @@ package module import ( - "testing" + "testing" + + "github.com/stretchr/testify/assert" ) func TestGetString(tester *testing.T) { - options := make(map[string]interface{}) - _, err := GetString(options, "MyKey") - if err == nil { - tester.Errorf("expected GetString error") - } - options["MyKey"] = "MyValue" - actual, err := GetString(options, "MyKey") - if err != nil { - tester.Errorf("unexpected GetString error") - } - if actual != "MyValue" { - tester.Errorf("expected GetString to return %s but got %s", "MyValue", actual) - } + options := make(map[string]interface{}) + _, err := GetString(options, "MyKey") + assert.Error(tester, err) + + options["MyKey"] = "MyValue" + actual, err := GetString(options, "MyKey") + if assert.Nil(tester, err) { + assert.Equal(tester, "MyValue", actual) + } + } func TestGetStringDefault(tester *testing.T) { - options := make(map[string]interface{}) - actual := GetStringDefault(options, "MyKey", "MyValue") - if actual != "MyValue" { - tester.Errorf("expected GetStringDefault to return %s but got %s", "MyValue", actual) - } - options["MyKey"] = "YourValue" - actual = GetStringDefault(options, "MyKey", "MyValue") - if actual != "YourValue" { - tester.Errorf("expected GetStringDefault to return %s but got %s", "YourValue", actual) - } + options := make(map[string]interface{}) + actual := GetStringDefault(options, "MyKey", "MyValue") + assert.Equal(tester, "MyValue", actual) + options["MyKey"] = "YourValue" + actual = GetStringDefault(options, "MyKey", "MyValue") + assert.Equal(tester, "YourValue", actual) } func TestGetInt(tester *testing.T) { - options := make(map[string]interface{}) - _, err := GetInt(options, "MyKey") - if err == nil { - tester.Errorf("expected GetInt error") - } - options["MyKey"] = float64(123) - actual, err := GetInt(options, "MyKey") - if err != nil { - tester.Errorf("unexpected GetInt error") - } - if actual != 123 { - tester.Errorf("expected GetInt to return %d but got %d", 123, actual) - } + options := make(map[string]interface{}) + _, err := GetInt(options, "MyKey") + assert.Error(tester, err) + options["MyKey"] = float64(123) + actual, err := GetInt(options, "MyKey") + if assert.Nil(tester, err) { + assert.Equal(tester, 123, actual) + } + } func TestGetIntDefault(tester *testing.T) { - options := make(map[string]interface{}) - actual := GetIntDefault(options, "MyKey", 123) - if actual != 123 { - tester.Errorf("expected GetIntDefault to return %d but got %d", 123, actual) - } - options["MyKey"] = float64(1234) - actual = GetIntDefault(options, "MyKey", 123) - if actual != 1234 { - tester.Errorf("expected GetIntDefault to return %d but got %d", 1234, actual) - } + options := make(map[string]interface{}) + actual := GetIntDefault(options, "MyKey", 123) + assert.Equal(tester, 123, actual) + options["MyKey"] = float64(1234) + actual = GetIntDefault(options, "MyKey", 123) + assert.Equal(tester, 1234, actual) } func TestGetBool(tester *testing.T) { - options := make(map[string]interface{}) - _, err := GetBool(options, "MyKey") - if err == nil { - tester.Errorf("expected GetBool error") - } - options["MyKey"] = true - actual, err := GetBool(options, "MyKey") - if err != nil { - tester.Errorf("unexpected GetBool error") - } - if actual != true { - tester.Errorf("expected GetBool to return %t but got %t", true, actual) - } + options := make(map[string]interface{}) + _, err := GetBool(options, "MyKey") + assert.Error(tester, err) + options["MyKey"] = true + actual, err := GetBool(options, "MyKey") + if assert.Nil(tester, err) { + assert.True(tester, actual) + } } func TestGetBoolDefault(tester *testing.T) { - options := make(map[string]interface{}) - actual := GetBoolDefault(options, "MyKey", true) - if actual != true { - tester.Errorf("expected GetBoolDefault to return %t but got %t", true, actual) - } - options["MyKey"] = false - actual = GetBoolDefault(options, "MyKey", true) - if actual != false { - tester.Errorf("expected GetBoolDefault to return %t but got %t", false, actual) - } + options := make(map[string]interface{}) + actual := GetBoolDefault(options, "MyKey", true) + assert.True(tester, actual) + options["MyKey"] = false + actual = GetBoolDefault(options, "MyKey", true) + assert.False(tester, actual) } func TestGetStringArray(tester *testing.T) { - options := make(map[string]interface{}) - _, err := GetStringArray(options, "MyKey") - if err == nil { - tester.Errorf("expected GetStringArray error") - } - array := make([]interface{}, 2, 2) - array[0] = "MyValue1" - array[1] = "MyValue2" - options["MyKey"] = array - actual, err := GetStringArray(options, "MyKey") - if err != nil { - tester.Errorf("unexpected GetString error") - } - if actual[0] != "MyValue1" { - tester.Errorf("expected GetString to return %s but got %s", "MyValue1", actual[0]) - } - if actual[1] != "MyValue2" { - tester.Errorf("expected GetString to return %s but got %s", "MyValue2", actual[1]) - } + options := make(map[string]interface{}) + _, err := GetStringArray(options, "MyKey") + assert.Error(tester, err) + array := make([]interface{}, 2, 2) + array[0] = "MyValue1" + array[1] = "MyValue2" + options["MyKey"] = array + actual, err := GetStringArray(options, "MyKey") + if assert.Nil(tester, err) { + assert.Equal(tester, "MyValue1", actual[0]) + assert.Equal(tester, "MyValue2", actual[1]) + } } func TestGetStringArrayDefault(tester *testing.T) { - options := make(map[string]interface{}) - actual := GetStringArrayDefault(options, "MyKey", make([]string, 0, 0)) - if len(actual) != 0 { - tester.Errorf("expected empty default string array but got %v", actual) - } + options := make(map[string]interface{}) + actual := GetStringArrayDefault(options, "MyKey", make([]string, 0, 0)) + assert.Len(tester, actual, 0) - array := make([]interface{}, 2, 2) - array[0] = "MyValue1" - array[1] = "MyValue2" - options["MyKey"] = array - actual = GetStringArrayDefault(options, "MyKey", make([]string, 0, 0)) - if actual[0] != "MyValue1" { - tester.Errorf("expected GetString to return %s but got %s", "MyValue1", actual[0]) - } - if actual[1] != "MyValue2" { - tester.Errorf("expected GetString to return %s but got %s", "MyValue2", actual[1]) - } + array := make([]interface{}, 2, 2) + array[0] = "MyValue1" + array[1] = "MyValue2" + options["MyKey"] = array + actual = GetStringArrayDefault(options, "MyKey", make([]string, 0, 0)) + assert.Equal(tester, "MyValue1", actual[0]) + assert.Equal(tester, "MyValue2", actual[1]) } diff --git a/packet/parser_test.go b/packet/parser_test.go index 65a7caf6..e80d0cdd 100644 --- a/packet/parser_test.go +++ b/packet/parser_test.go @@ -11,38 +11,31 @@ package packet import ( - "io/ioutil" - "os" - "testing" - "github.com/google/gopacket" - "github.com/security-onion-solutions/securityonion-soc/model" + "io/ioutil" + "os" + "testing" + + "github.com/google/gopacket" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/stretchr/testify/assert" ) func TestOverrideType(tester *testing.T) { - p := model.NewPacket(1) - p.Type = "foo" - overrideType(p, gopacket.LayerTypePayload) - if p.Type != "foo" { - tester.Errorf("expected Type %s but got %s", "foo", p.Type) - } - overrideType(p, gopacket.LayerTypeFragment) - if p.Type != "Fragment" { - tester.Errorf("expected Type %s but got %s", "Fragment", p.Type) - } + p := model.NewPacket(1) + p.Type = "foo" + overrideType(p, gopacket.LayerTypePayload) + assert.Equal(tester, "foo", p.Type) + overrideType(p, gopacket.LayerTypeFragment) + assert.Equal(tester, "Fragment", p.Type) } func TestUnwrapPcap(tester *testing.T) { - filename := "parser_resource.pcap" - tmpFile, err := ioutil.TempFile("", "unwrap-test") - if err != nil { - tester.Errorf("Unable to execute test due to bad temp file: %-v", err) - return - } - unwrappedFilename := tmpFile.Name() - os.Remove(unwrappedFilename) // Don't need the actual file right now, delete it. We only need a filename. - defer os.Remove(unwrappedFilename) // Delete it again after test finishes. - unwrapped := UnwrapPcap(filename, unwrappedFilename) - if !unwrapped { - tester.Errorf("expected unwrap to succeed") - } + filename := "parser_resource.pcap" + tmpFile, err := ioutil.TempFile("", "unwrap-test") + assert.Nil(tester, err, "Unable to execute test due to bad temp file") + unwrappedFilename := tmpFile.Name() + os.Remove(unwrappedFilename) // Don't need the actual file right now, delete it. We only need a filename. + defer os.Remove(unwrappedFilename) // Delete it again after test finishes. + unwrapped := UnwrapPcap(filename, unwrappedFilename) + assert.True(tester, unwrapped) } diff --git a/server/modules/elastic/converter_test.go b/server/modules/elastic/converter_test.go index d8b1d563..4ac96d40 100644 --- a/server/modules/elastic/converter_test.go +++ b/server/modules/elastic/converter_test.go @@ -11,231 +11,143 @@ package elastic import ( - "io/ioutil" - "testing" - "time" + "io/ioutil" + "testing" + "time" - "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/model" "github.com/stretchr/testify/assert" ) func NewTestStore() *ElasticEventstore { - return &ElasticEventstore{ - fieldDefs: make(map[string]*FieldDefinition), - intervals: DEFAULT_INTERVALS, - } + return &ElasticEventstore{ + fieldDefs: make(map[string]*FieldDefinition), + intervals: DEFAULT_INTERVALS, + } } func TestMakeAggregation(tester *testing.T) { - keys := []string{"one*","two","three*"} - agg, name := makeAggregation(NewTestStore(), "groupby", keys, 10, false) - if name != "groupby|one" { - tester.Errorf("Expected aggregation name %s but got %s", "groupby|one", name) - } - if agg["terms"] == nil { - tester.Errorf("aggregation missing terms") - } - terms := agg["terms"].(map[string]interface{}) - if terms["field"] != "one" { - tester.Errorf("Expected %s, Actual %s", "one", terms["field"]) - } - if terms["missing"] != "__missing__" { - tester.Errorf("Expected %s, Actual %s", "__missing__", terms["missing"]) - } - if terms["size"] != 10 { - tester.Errorf("Expected %d, Actual %d", 10, terms["size"]) - } - if terms["order"] == nil { - tester.Errorf("aggregation missing order") - } - order := terms["order"].(map[string]interface{}) - if order["_count"] != "desc" { - tester.Errorf("Expected %s, Actual %s", "desc", terms["order"]) - } - if agg["aggs"] == nil { - tester.Errorf("aggregation missing nested aggregations") - } - secondAggs := agg["aggs"].(map[string]interface{}) - if secondAggs["groupby|one|two"] == nil { - tester.Errorf("Nested aggregation missing 'groupby|one|two' key") - } - secondAgg := secondAggs["groupby|one|two"].(map[string]interface{}) - if secondAgg["aggs"] == nil { - tester.Errorf("aggregation missing second level aggregations") - } - terms = secondAgg["terms"].(map[string]interface{}) - if terms["missing"] != nil { - tester.Errorf("Unexpected missing term, Actual %v", terms["missing"]) - } - - thirdAggs := secondAgg["aggs"].(map[string]interface{}) - if thirdAggs["groupby|one|two|three"] == nil { - tester.Errorf("Nested aggregation missing 'groupby|one|two|three' key") - } - thirdAgg := thirdAggs["groupby|one|two|three"].(map[string]interface{}) - if thirdAgg["aggs"] != nil { - tester.Errorf("third aggregation should not have another level aggregation") - } - terms = thirdAgg["terms"].(map[string]interface{}) - if terms["missing"] != "__missing__" { - tester.Errorf("Expected third agg %s, Actual %s", "__missing__", terms["missing"]) - } - + keys := []string{"one*", "two", "three*"} + agg, name := makeAggregation(NewTestStore(), "groupby", keys, 10, false) + assert.Equal(tester, "groupby|one", name) + + assert.NotNil(tester, agg["terms"]) + terms := agg["terms"].(map[string]interface{}) + assert.Equal(tester, "one", terms["field"]) + assert.Equal(tester, "__missing__", terms["missing"]) + assert.Equal(tester, 10, terms["size"]) + + assert.NotNil(tester, terms["order"]) + order := terms["order"].(map[string]interface{}) + assert.Equal(tester, "desc", order["_count"]) + + assert.NotNil(tester, agg["aggs"]) + secondAggs := agg["aggs"].(map[string]interface{}) + assert.NotNil(tester, secondAggs["groupby|one|two"]) + secondAgg := secondAggs["groupby|one|two"].(map[string]interface{}) + assert.NotNil(tester, secondAgg["aggs"]) + terms = secondAgg["terms"].(map[string]interface{}) + assert.Nil(tester, terms["missing"]) + + thirdAggs := secondAgg["aggs"].(map[string]interface{}) + assert.NotNil(tester, thirdAggs["groupby|one|two|three"]) + thirdAgg := thirdAggs["groupby|one|two|three"].(map[string]interface{}) + assert.Nil(tester, thirdAgg["aggs"]) + terms = thirdAgg["terms"].(map[string]interface{}) + assert.Equal(tester, "__missing__", terms["missing"]) } - func TestMakeTimeline(tester *testing.T) { - timeline := makeTimeline("30m") - if timeline["date_histogram"] == nil { - tester.Errorf("timeline missing date_histogram") - } - terms := timeline["date_histogram"].(map[string]interface{}) - if terms["field"] != "@timestamp" { - tester.Errorf("Expected %s, Actual %s", "@timestamp", terms["field"]) - } - if terms["fixed_interval"] != "30m" { - tester.Errorf("Expected %s, Actual %s", "30m", terms["fixed_interval"]) - } - if terms["min_doc_count"] != 1 { - tester.Errorf("Expected %d, Actual %d", 1, terms["min_doc_count"]) - } + timeline := makeTimeline("30m") + assert.NotNil(tester, timeline["date_histogram"]) + terms := timeline["date_histogram"].(map[string]interface{}) + assert.Equal(tester, "@timestamp", terms["field"]) + assert.Equal(tester, "30m", terms["fixed_interval"]) + assert.Equal(tester, 1, terms["min_doc_count"]) } func TestCalcTimelineInterval(tester *testing.T) { - start, _ := time.Parse(time.RFC3339, "2021-01-02T05:00:00Z") - end, _ := time.Parse(time.RFC3339, "2021-01-02T13:00:00Z") - interval := calcTimelineInterval(25, start, end) - if interval != "15m" { - tester.Errorf("Expected 15m interval but got %s", interval) - } - - // Boundaries - start, _ = time.Parse(time.RFC3339, "2021-01-02T13:00:00Z") - end, _ = time.Parse(time.RFC3339, "2021-01-02T13:00:01Z") - interval = calcTimelineInterval(25, start, end) - if interval != "1s" { - tester.Errorf("Expected 1s interval but got %s", interval) - } - - start, _ = time.Parse(time.RFC3339, "1990-01-02T05:00:00Z") - end, _ = time.Parse(time.RFC3339, "2021-01-02T13:00:00Z") - interval = calcTimelineInterval(25, start, end) - if interval != "30d" { - tester.Errorf("Expected 30d interval but got %s", interval) - } + start, _ := time.Parse(time.RFC3339, "2021-01-02T05:00:00Z") + end, _ := time.Parse(time.RFC3339, "2021-01-02T13:00:00Z") + interval := calcTimelineInterval(25, start, end) + assert.Equal(tester, "15m", interval) + + // Boundaries + start, _ = time.Parse(time.RFC3339, "2021-01-02T13:00:00Z") + end, _ = time.Parse(time.RFC3339, "2021-01-02T13:00:01Z") + interval = calcTimelineInterval(25, start, end) + assert.Equal(tester, "1s", interval) + + start, _ = time.Parse(time.RFC3339, "1990-01-02T05:00:00Z") + end, _ = time.Parse(time.RFC3339, "2021-01-02T13:00:00Z") + interval = calcTimelineInterval(25, start, end) + assert.Equal(tester, "30d", interval) } func TestConvertToElasticRequestEmptyCriteria(tester *testing.T) { - criteria := model.NewEventSearchCriteria() - actualJson, err := convertToElasticRequest(NewTestStore(), criteria) - if err != nil { - tester.Errorf("unexpected conversion error: %s", err) - } - - expectedJson := `{"aggs":{"timeline":{"date_histogram":{"field":"@timestamp","fixed_interval":"1s","min_doc_count":1}}},"query":{"bool":{"filter":[],"must":[{"query_string":{"analyze_wildcard":true,"default_field":"*","query":"*"}},{"range":{"@timestamp":{"format":"strict_date_optional_time","gte":"0001-01-01T00:00:00Z","lte":"0001-01-01T00:00:00Z"}}}],"must_not":[],"should":[]}},"size":25}` - if actualJson != expectedJson { - tester.Errorf("Mismatched ES request conversion; actual='%s' vs expected='%s'", actualJson, expectedJson) - } + criteria := model.NewEventSearchCriteria() + actualJson, err := convertToElasticRequest(NewTestStore(), criteria) + assert.Nil(tester, err) + + expectedJson := `{"aggs":{"timeline":{"date_histogram":{"field":"@timestamp","fixed_interval":"1s","min_doc_count":1}}},"query":{"bool":{"filter":[],"must":[{"query_string":{"analyze_wildcard":true,"default_field":"*","query":"*"}},{"range":{"@timestamp":{"format":"strict_date_optional_time","gte":"0001-01-01T00:00:00Z","lte":"0001-01-01T00:00:00Z"}}}],"must_not":[],"should":[]}},"size":25}` + assert.Equal(tester, expectedJson, actualJson) } func TestConvertToElasticRequestGroupByCriteria(tester *testing.T) { - criteria := model.NewEventSearchCriteria() - criteria.Populate(`abc AND def AND q:"\\\\file\\path" | groupby ghi jkl*`, "2020-01-02T12:13:14Z - 2020-01-02T13:13:14Z", time.RFC3339, "America/New_York", "10", "25") - actualJson, err := convertToElasticRequest(NewTestStore(), criteria) - if err != nil { - tester.Errorf("unexpected conversion error: %s", err) - } - - expectedJson := `{"aggs":{"bottom":{"terms":{"field":"ghi","order":{"_count":"asc"},"size":10}},"groupby|ghi":{"aggs":{"groupby|ghi|jkl":{"terms":{"field":"jkl","missing":"__missing__","order":{"_count":"desc"},"size":10}}},"terms":{"field":"ghi","order":{"_count":"desc"},"size":10}},"timeline":{"date_histogram":{"field":"@timestamp","fixed_interval":"1m","min_doc_count":1}}},"query":{"bool":{"filter":[],"must":[{"query_string":{"analyze_wildcard":true,"default_field":"*","query":"abc AND def AND q: \"\\\\\\\\file\\\\path\""}},{"range":{"@timestamp":{"format":"strict_date_optional_time","gte":"2020-01-02T12:13:14Z","lte":"2020-01-02T13:13:14Z"}}}],"must_not":[],"should":[]}},"size":25}` - if actualJson != expectedJson { - tester.Errorf("Mismatched ES request conversion; actual='%s' vs expected='%s'", actualJson, expectedJson) - } + criteria := model.NewEventSearchCriteria() + criteria.Populate(`abc AND def AND q:"\\\\file\\path" | groupby ghi jkl*`, "2020-01-02T12:13:14Z - 2020-01-02T13:13:14Z", time.RFC3339, "America/New_York", "10", "25") + actualJson, err := convertToElasticRequest(NewTestStore(), criteria) + assert.Nil(tester, err) + + expectedJson := `{"aggs":{"bottom":{"terms":{"field":"ghi","order":{"_count":"asc"},"size":10}},"groupby|ghi":{"aggs":{"groupby|ghi|jkl":{"terms":{"field":"jkl","missing":"__missing__","order":{"_count":"desc"},"size":10}}},"terms":{"field":"ghi","order":{"_count":"desc"},"size":10}},"timeline":{"date_histogram":{"field":"@timestamp","fixed_interval":"1m","min_doc_count":1}}},"query":{"bool":{"filter":[],"must":[{"query_string":{"analyze_wildcard":true,"default_field":"*","query":"abc AND def AND q: \"\\\\\\\\file\\\\path\""}},{"range":{"@timestamp":{"format":"strict_date_optional_time","gte":"2020-01-02T12:13:14Z","lte":"2020-01-02T13:13:14Z"}}}],"must_not":[],"should":[]}},"size":25}` + assert.Equal(tester, expectedJson, actualJson) } func TestConvertFromElasticResultsSuccess(tester *testing.T) { - esData, err := ioutil.ReadFile("converter_response.json") - if err != nil { - tester.Errorf("unexpected test setup error: %s", err) - } - - results := model.NewEventSearchResults() - err = convertFromElasticResults(NewTestStore(), string(esData), results) - if err != nil { - tester.Errorf("unexpected conversion error: %s", err) - } - - if results.ElapsedMs != 9534 { - tester.Errorf("Failed to parse ElapsedMs (%d): %s", results.ElapsedMs, err) - } - - if results.TotalEvents != 23689430 { - tester.Errorf("Unexpected total events: %d", results.TotalEvents) - } - - if len(results.Events) != 25 { - tester.Errorf("Unexpected returned event count: %d", len(results.Events)) - } - - if results.Events[0].Timestamp != "2020-04-24T03:00:55.300Z" { - tester.Errorf("Unexpected timestamp: %-v", results.Events[0].Timestamp) - } - - if results.Events[0].Source != "so16:logstash-bro-2020.04.24" { - tester.Errorf("Unexpected source: %s", results.Events[0].Source) - } - - if results.Metrics["groupby|source_ip"] == nil { - tester.Errorf("Missing outer groupby metric") - } - - if results.Metrics["groupby|source_ip|destination_ip"] == nil { - tester.Errorf("Missing outer groupby metric") - } - - if results.Metrics["groupby|source_ip|destination_ip|protocol"] == nil { - tester.Errorf("Missing outer groupby metric") - } - - if results.Metrics["groupby|source_ip|destination_ip|protocol|destination_port"] == nil { - tester.Errorf("Missing outer groupby metric") - } + esData, err := ioutil.ReadFile("converter_response.json") + assert.Nil(tester, err) + + results := model.NewEventSearchResults() + err = convertFromElasticResults(NewTestStore(), string(esData), results) + if assert.Nil(tester, err) { + assert.Equal(tester, 9534, results.ElapsedMs) + assert.Equal(tester, 23689430, results.TotalEvents) + assert.Len(tester, results.Events, 25) + assert.Equal(tester, "2020-04-24T03:00:55.300Z", results.Events[0].Timestamp) + assert.Equal(tester, "so16:logstash-bro-2020.04.24", results.Events[0].Source) + assert.NotNil(tester, results.Metrics["groupby|source_ip"]) + assert.NotNil(tester, results.Metrics["groupby|source_ip|destination_ip"]) + assert.NotNil(tester, results.Metrics["groupby|source_ip|destination_ip|protocol"]) + assert.NotNil(tester, results.Metrics["groupby|source_ip|destination_ip|protocol|destination_port"]) + } + } func TestConvertFromElasticResultsTimedOut(tester *testing.T) { - results := model.NewEventSearchResults() - err := convertFromElasticResults(NewTestStore(), `{ "took": 123, "timed_out": true, "hits": {} }`, results) - if err == nil { - tester.Errorf("Expected timed out results") - } - - if results.ElapsedMs != 123 { - tester.Errorf("Failed to parse ElapsedMs (%d) regardless of timed_out flag: %s", results.ElapsedMs, err) - } + results := model.NewEventSearchResults() + err := convertFromElasticResults(NewTestStore(), `{ "took": 123, "timed_out": true, "hits": {} }`, results) + assert.Error(tester, err) + + assert.Equal(tester, 123, results.ElapsedMs, "ElapsedMs should exist even on timeout.") } func TestConvertFromElasticResultsInvalid(tester *testing.T) { - results := model.NewEventSearchResults() - err := convertFromElasticResults(NewTestStore(), `{ }`, results) - if err == nil { - tester.Errorf("Expected invalid results error") - } + results := model.NewEventSearchResults() + err := convertFromElasticResults(NewTestStore(), `{ }`, results) + assert.Error(tester, err) } func TestConvertToElasticUpdateRequest(tester *testing.T) { - criteria := model.NewEventUpdateCriteria() - criteria.AddUpdateScript("ctx._source.event.acknowledged=true") - criteria.AddUpdateScript("ctx._source.event.escalated=true") - criteria.Populate("event.dataset:alerts", "2020/09/24 10:11:12 AM - 2020/09/24 12:14:15 PM", "2006/01/02 3:04:05 PM", "America/New_York", "0", "0"); - - actualJson, err := convertToElasticUpdateRequest(NewTestStore(), criteria) - if err != nil { - tester.Errorf("unexpected conversion error: %s", err) - } - - expectedJson := `{"query":{"bool":{"filter":[],"must":[{"query_string":{"analyze_wildcard":true,"default_field":"*","query":"event.dataset:alerts"}},{"range":{"@timestamp":{"format":"strict_date_optional_time","gte":"2020-09-24T10:11:12-04:00","lte":"2020-09-24T12:14:15-04:00"}}}],"must_not":[],"should":[]}},"script":{"inline":"ctx._source.event.acknowledged=true; ctx._source.event.escalated=true","lang":"painless"}}` - if actualJson != expectedJson { - tester.Errorf("Mismatched ES request conversion; actual='%s' vs expected='%s'", actualJson, expectedJson) - } + criteria := model.NewEventUpdateCriteria() + criteria.AddUpdateScript("ctx._source.event.acknowledged=true") + criteria.AddUpdateScript("ctx._source.event.escalated=true") + criteria.Populate("event.dataset:alerts", "2020/09/24 10:11:12 AM - 2020/09/24 12:14:15 PM", "2006/01/02 3:04:05 PM", "America/New_York", "0", "0") + + actualJson, err := convertToElasticUpdateRequest(NewTestStore(), criteria) + assert.Nil(tester, err) + + expectedJson := `{"query":{"bool":{"filter":[],"must":[{"query_string":{"analyze_wildcard":true,"default_field":"*","query":"event.dataset:alerts"}},{"range":{"@timestamp":{"format":"strict_date_optional_time","gte":"2020-09-24T10:11:12-04:00","lte":"2020-09-24T12:14:15-04:00"}}}],"must_not":[],"should":[]}},"script":{"inline":"ctx._source.event.acknowledged=true; ctx._source.event.escalated=true","lang":"painless"}}` + assert.Equal(tester, expectedJson, actualJson) } const updateResponse = `{ @@ -258,34 +170,24 @@ const updateResponse = `{ }` func TestConvertFromElasticUpdateResultsSuccess(tester *testing.T) { - results := model.NewEventUpdateResults() - err := convertFromElasticUpdateResults(NewTestStore(), updateResponse, results) - if err != nil { - tester.Errorf("unexpected conversion error: %s", err) - } - - if results.ElapsedMs != 202 { - tester.Errorf("Failed to parse ElapsedMs (%d): %s", results.ElapsedMs, err) - } - - if results.UpdatedCount != 3 { - tester.Errorf("Unexpected updated count: %d", results.UpdatedCount) - } - - if results.UnchangedCount != 2 { - tester.Errorf("Unexpected updated count: %d", results.UnchangedCount) - } + results := model.NewEventUpdateResults() + err := convertFromElasticUpdateResults(NewTestStore(), updateResponse, results) + if assert.Nil(tester, err) { + assert.Equal(tester, 202, results.ElapsedMs) + assert.Equal(tester, 3, results.UpdatedCount) + assert.Equal(tester, 2, results.UnchangedCount) + } } func TestAddEventUpdateResults(tester *testing.T) { - results1 := model.NewEventUpdateResults() - results1.ElapsedMs = 100 - results1.UpdatedCount = 200 - results1.UnchangedCount = 400 - results2 := model.NewEventUpdateResults() - results2.ElapsedMs = 12 - results2.UpdatedCount = 2 - results2.UnchangedCount = 4 + results1 := model.NewEventUpdateResults() + results1.ElapsedMs = 100 + results1.UpdatedCount = 200 + results1.UnchangedCount = 400 + results2 := model.NewEventUpdateResults() + results2.ElapsedMs = 12 + results2.UpdatedCount = 2 + results2.UnchangedCount = 4 results1.AddEventUpdateResults(results2) assert.Equal(tester, 112, results1.ElapsedMs) @@ -294,39 +196,34 @@ func TestAddEventUpdateResults(tester *testing.T) { } func validateFormatSearch(tester *testing.T, original string, expected string) { - output := formatSearch(original) - if output != expected { - tester.Errorf("Expected mapped query '%s' but got '%s'", expected, output) - } + output := formatSearch(original) + assert.Equal(tester, expected, output) } func TestFormatSearch(tester *testing.T) { - validateFormatSearch(tester, "", "*") - validateFormatSearch(tester, " ", "*") - validateFormatSearch(tester, "\\foo\\bar", "\\foo\\bar") + validateFormatSearch(tester, "", "*") + validateFormatSearch(tester, " ", "*") + validateFormatSearch(tester, "\\foo\\bar", "\\foo\\bar") } - func validateMappedQuery(tester *testing.T, original string, expected string) { - store := NewTestStore() - store.fieldDefs["foo"] = &FieldDefinition { aggregatable: false, } - store.fieldDefs["foo.keyword"] = &FieldDefinition { aggregatable: true, } - query := model.NewQuery() - query.Parse(original) - search := query.NamedSegment(model.SegmentKind_Search) - mapSearch(store, search.(*model.SearchSegment)) - output := search.String() - if output != expected { - tester.Errorf("Expected mapped query '%s' but got '%s'", expected, output) - } + store := NewTestStore() + store.fieldDefs["foo"] = &FieldDefinition{aggregatable: false} + store.fieldDefs["foo.keyword"] = &FieldDefinition{aggregatable: true} + query := model.NewQuery() + query.Parse(original) + search := query.NamedSegment(model.SegmentKind_Search) + mapSearch(store, search.(*model.SearchSegment)) + output := search.String() + assert.Equal(tester, expected, output) } func TestMapSearch(tester *testing.T) { - validateMappedQuery(tester, "foo: \"bar\"", "foo.keyword: \"bar\"") - validateMappedQuery(tester, "foo: \"bar\" AND barfoo: \"blue\"", "foo.keyword: \"bar\" AND barfoo: \"blue\"") - validateMappedQuery(tester, "foo: 123", "foo.keyword: 123") - validateMappedQuery(tester, "(foo: \"123\")", "(foo: \"123\")") - validateMappedQuery(tester, "foo2: \"bar\"", "foo2: \"bar\"") - validateMappedQuery(tester, "barfoo: \"bar\"", "barfoo: \"bar\"") - validateMappedQuery(tester, "barfoo: \"foo: bar\"", "barfoo: \"foo: bar\"") + validateMappedQuery(tester, "foo: \"bar\"", "foo.keyword: \"bar\"") + validateMappedQuery(tester, "foo: \"bar\" AND barfoo: \"blue\"", "foo.keyword: \"bar\" AND barfoo: \"blue\"") + validateMappedQuery(tester, "foo: 123", "foo.keyword: 123") + validateMappedQuery(tester, "(foo: \"123\")", "(foo: \"123\")") + validateMappedQuery(tester, "foo2: \"bar\"", "foo2: \"bar\"") + validateMappedQuery(tester, "barfoo: \"bar\"", "barfoo: \"bar\"") + validateMappedQuery(tester, "barfoo: \"foo: bar\"", "barfoo: \"foo: bar\"") } diff --git a/server/modules/elastic/elastic_test.go b/server/modules/elastic/elastic_test.go index d78ae894..b88171ba 100644 --- a/server/modules/elastic/elastic_test.go +++ b/server/modules/elastic/elastic_test.go @@ -11,46 +11,31 @@ package elastic import ( - "testing" - "time" - "github.com/security-onion-solutions/securityonion-soc/module" + "testing" + "time" + + "github.com/security-onion-solutions/securityonion-soc/module" + "github.com/stretchr/testify/assert" ) func TestElasticInit(tester *testing.T) { - elastic := NewElastic(nil) - cfg := make(module.ModuleConfig) - err := elastic.Init(cfg) - if err != nil { - tester.Errorf("unexpected Init error: %s", err) - } - if len(elastic.store.hostUrls) != 1 || elastic.store.hostUrls[0] != "elasticsearch" { - tester.Errorf("expected host %s but got %s", "elasticsearch", elastic.store.hostUrls[0]) - } - if len(elastic.store.esRemoteClients) != 0 { - tester.Errorf("expected no remote hosts but got %v", elastic.store.esRemoteClients) - } - if len(elastic.store.esAllClients) != 1 { - tester.Errorf("expected no remote hosts but got %v", elastic.store.esAllClients) - } - if elastic.store.timeShiftMs != DEFAULT_TIME_SHIFT_MS { - tester.Errorf("expected timeShiftMs %d but got %d", DEFAULT_TIME_SHIFT_MS, elastic.store.timeShiftMs) - } - if elastic.store.defaultDurationMs != DEFAULT_DURATION_MS { - tester.Errorf("expected defaultDurationMs %d but got %d", DEFAULT_DURATION_MS, elastic.store.defaultDurationMs) - } - if elastic.store.esSearchOffsetMs != DEFAULT_ES_SEARCH_OFFSET_MS { - tester.Errorf("expected esSearchOffsetMs %d but got %d", DEFAULT_ES_SEARCH_OFFSET_MS, elastic.store.esSearchOffsetMs) - } - if elastic.store.timeoutMs != time.Duration(DEFAULT_TIMEOUT_MS) * time.Millisecond { - tester.Errorf("expected timeoutMs %d but got %d", DEFAULT_TIMEOUT_MS, elastic.store.timeoutMs) - } - if elastic.store.cacheMs != time.Duration(DEFAULT_CACHE_MS) * time.Millisecond { - tester.Errorf("expected cacheMs %d but got %d", DEFAULT_CACHE_MS, elastic.store.cacheMs) - } - if elastic.store.index != DEFAULT_INDEX { - tester.Errorf("expected index %s but got %s", DEFAULT_INDEX, elastic.store.index) - } - if elastic.store.intervals != DEFAULT_INTERVALS { - tester.Errorf("expected interval %d but got %d", DEFAULT_INTERVALS, elastic.store.intervals) - } + elastic := NewElastic(nil) + cfg := make(module.ModuleConfig) + err := elastic.Init(cfg) + if assert.Nil(tester, err) { + if assert.Len(tester, elastic.store.hostUrls, 1) { + assert.Equal(tester, "elasticsearch", elastic.store.hostUrls[0]) + } + assert.Len(tester, elastic.store.esRemoteClients, 0) + assert.Len(tester, elastic.store.esAllClients, 1) + assert.Equal(tester, DEFAULT_TIME_SHIFT_MS, elastic.store.timeShiftMs) + assert.Equal(tester, DEFAULT_DURATION_MS, elastic.store.defaultDurationMs) + assert.Equal(tester, DEFAULT_ES_SEARCH_OFFSET_MS, elastic.store.esSearchOffsetMs) + expectedTimeout := time.Duration(DEFAULT_TIMEOUT_MS) * time.Millisecond + assert.Equal(tester, expectedTimeout, elastic.store.timeoutMs) + expectedCache := time.Duration(DEFAULT_CACHE_MS) * time.Millisecond + assert.Equal(tester, expectedCache, elastic.store.cacheMs) + assert.Equal(tester, DEFAULT_INDEX, elastic.store.index) + assert.Equal(tester, DEFAULT_INTERVALS, elastic.store.intervals) + } } diff --git a/server/modules/elastic/elasticeventstore.go b/server/modules/elastic/elasticeventstore.go index 7709788e..f7bc38b3 100644 --- a/server/modules/elastic/elasticeventstore.go +++ b/server/modules/elastic/elasticeventstore.go @@ -11,462 +11,463 @@ package elastic import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "math" - "strconv" - "strings" - "sync" - "time" - "github.com/apex/log" - "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" + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "math" + "strconv" + "strings" + "sync" + "time" + + "github.com/apex/log" + "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" ) const MAX_ERROR_LENGTH = 4096 type FieldDefinition struct { - name string - fieldType string - aggregatable bool - searchable bool + name string + fieldType string + aggregatable bool + searchable bool } type ElasticEventstore struct { - hostUrls []string - esClient *elasticsearch.Client - esRemoteClients []*elasticsearch.Client - esAllClients []*elasticsearch.Client - timeShiftMs int - defaultDurationMs int - esSearchOffsetMs int - timeoutMs time.Duration - index string - cacheMs time.Duration - cacheTime time.Time - cacheLock sync.Mutex - fieldDefs map[string]*FieldDefinition - intervals int - asyncThreshold int + hostUrls []string + esClient *elasticsearch.Client + esRemoteClients []*elasticsearch.Client + esAllClients []*elasticsearch.Client + timeShiftMs int + defaultDurationMs int + esSearchOffsetMs int + timeoutMs time.Duration + index string + cacheMs time.Duration + cacheTime time.Time + cacheLock sync.Mutex + fieldDefs map[string]*FieldDefinition + intervals int + asyncThreshold int } func NewElasticEventstore() *ElasticEventstore { - return &ElasticEventstore{ - hostUrls: make([]string, 0), - esRemoteClients: make([]*elasticsearch.Client, 0), - esAllClients: make([]*elasticsearch.Client, 0), - } + return &ElasticEventstore{ + hostUrls: make([]string, 0), + esRemoteClients: make([]*elasticsearch.Client, 0), + esAllClients: make([]*elasticsearch.Client, 0), + } } func (store *ElasticEventstore) Init(hostUrl string, - remoteHosts []string, - user string, - pass string, - verifyCert bool, - timeShiftMs int, - defaultDurationMs int, - esSearchOffsetMs int, - timeoutMs int, - cacheMs int, - index string, - asyncThreshold int, - intervals int) error { - store.timeShiftMs = timeShiftMs - store.defaultDurationMs = defaultDurationMs - store.esSearchOffsetMs = esSearchOffsetMs - store.index = index - store.asyncThreshold = asyncThreshold - store.timeoutMs = time.Duration(timeoutMs) * time.Millisecond - store.cacheMs = time.Duration(cacheMs) * time.Millisecond - store.intervals = intervals - - var err error - store.esClient, err = store.makeEsClient(hostUrl, user, pass, verifyCert) - if err == nil { - store.hostUrls = append(store.hostUrls, hostUrl) - store.esAllClients = append(store.esAllClients, store.esClient) - for _, remoteHostUrl := range(remoteHosts) { - client, err := store.makeEsClient(remoteHostUrl, user, pass, verifyCert) - if err == nil { - store.hostUrls = append(store.hostUrls, remoteHostUrl) - store.esRemoteClients = append(store.esRemoteClients, client) - store.esAllClients = append(store.esAllClients, client) - } else { - break - } - } - } - return err + remoteHosts []string, + user string, + pass string, + verifyCert bool, + timeShiftMs int, + defaultDurationMs int, + esSearchOffsetMs int, + timeoutMs int, + cacheMs int, + index string, + asyncThreshold int, + intervals int) error { + store.timeShiftMs = timeShiftMs + store.defaultDurationMs = defaultDurationMs + store.esSearchOffsetMs = esSearchOffsetMs + store.index = index + store.asyncThreshold = asyncThreshold + store.timeoutMs = time.Duration(timeoutMs) * time.Millisecond + store.cacheMs = time.Duration(cacheMs) * time.Millisecond + store.intervals = intervals + + var err error + store.esClient, err = store.makeEsClient(hostUrl, user, pass, verifyCert) + if err == nil { + store.hostUrls = append(store.hostUrls, hostUrl) + store.esAllClients = append(store.esAllClients, store.esClient) + for _, remoteHostUrl := range remoteHosts { + client, err := store.makeEsClient(remoteHostUrl, user, pass, verifyCert) + if err == nil { + store.hostUrls = append(store.hostUrls, remoteHostUrl) + store.esRemoteClients = append(store.esRemoteClients, client) + store.esAllClients = append(store.esAllClients, client) + } else { + break + } + } + } + return err } func (store *ElasticEventstore) makeEsClient(host string, user string, pass string, verifyCert bool) (*elasticsearch.Client, error) { - var esClient *elasticsearch.Client - - hosts := make([]string, 1) - hosts[0] = host - esConfig := elasticsearch.Config { - Addresses: hosts, - Username: user, - Password: pass, - Transport: NewElasticTransport(user, pass, store.timeoutMs, verifyCert), - } - maskedPassword := "*****" - if len(esConfig.Password) == 0 { - maskedPassword = "" - } - - esClient, err := elasticsearch.NewClient(esConfig) - fields := log.Fields { - "InsecureSkipVerify": !verifyCert, - "HostUrl": host, - "Username": esConfig.Username, - "Password": maskedPassword, - "Index": store.index, - "TimeoutMs": store.timeoutMs, - } - if err == nil { - log.WithFields(fields).Info("Initialized Elasticsearch Client") - } else { - log.WithFields(fields).Error("Failed to initialize Elasticsearch Client") - esClient = nil - } - return esClient, err + var esClient *elasticsearch.Client + + hosts := make([]string, 1) + hosts[0] = host + esConfig := elasticsearch.Config{ + Addresses: hosts, + Username: user, + Password: pass, + Transport: NewElasticTransport(user, pass, store.timeoutMs, verifyCert), + } + maskedPassword := "*****" + if len(esConfig.Password) == 0 { + maskedPassword = "" + } + + esClient, err := elasticsearch.NewClient(esConfig) + fields := log.Fields{ + "InsecureSkipVerify": !verifyCert, + "HostUrl": host, + "Username": esConfig.Username, + "Password": maskedPassword, + "Index": store.index, + "TimeoutMs": store.timeoutMs, + } + if err == nil { + log.WithFields(fields).Info("Initialized Elasticsearch Client") + } else { + log.WithFields(fields).Error("Failed to initialize Elasticsearch Client") + esClient = nil + } + return esClient, err } func (store *ElasticEventstore) mapElasticField(field string) string { - mappedField := store.fieldDefs[field] - if mappedField != nil && !mappedField.aggregatable { - keyword := field + ".keyword" - mappedField = store.fieldDefs[keyword] - if mappedField != nil && mappedField.aggregatable { - field = keyword - } - } - return field + mappedField := store.fieldDefs[field] + if mappedField != nil && !mappedField.aggregatable { + keyword := field + ".keyword" + mappedField = store.fieldDefs[keyword] + if mappedField != nil && mappedField.aggregatable { + field = keyword + } + } + return field } func (store *ElasticEventstore) unmapElasticField(field string) string { - suffix := ".keyword" - if strings.HasSuffix(field, suffix) { - newField := strings.TrimSuffix(field, suffix) - mappedField := store.fieldDefs[newField] - if mappedField != nil && !mappedField.aggregatable { - field = newField - } - } - return field + suffix := ".keyword" + if strings.HasSuffix(field, suffix) { + newField := strings.TrimSuffix(field, suffix) + mappedField := store.fieldDefs[newField] + if mappedField != nil && !mappedField.aggregatable { + field = newField + } + } + return field } 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(ctx, query) - if err == nil { - err = convertFromElasticResults(store, response, results) - results.Criteria = criteria - } - } - - results.Complete() - return results, err + store.refreshCache(ctx) + + results := model.NewEventSearchResults() + query, err := convertToElasticRequest(store, criteria) + if err == nil { + var response string + response, err = store.luceneSearch(ctx, query) + if err == nil { + err = convertFromElasticResults(store, response, results) + results.Criteria = criteria + } + } + + results.Complete() + return results, err } func (store *ElasticEventstore) disableCrossClusterIndexing(indexes []string) []string { - for idx, index := range(indexes) { - pieces := strings.SplitN(index, ":", 2) - if len(pieces) == 2 { - indexes[idx] = pieces[1] - } - } - return indexes + for idx, index := range indexes { + pieces := strings.SplitN(index, ":", 2) + if len(pieces) == 2 { + indexes[idx] = pieces[1] + } + } + return indexes } func (store *ElasticEventstore) Update(ctx context.Context, criteria *model.EventUpdateCriteria) (*model.EventUpdateResults, error) { - store.refreshCache(ctx) - - results := model.NewEventUpdateResults() - results.Criteria = criteria - query, err := convertToElasticUpdateRequest(store, criteria) - if err == nil { - var response string - - for idx, client := range(store.esAllClients) { - log.WithField("clientHost", store.hostUrls[idx]).Debug("Sending request to client") - response, err = store.updateDocuments(ctx, client, query, store.disableCrossClusterIndexing(strings.Split(store.index, ",")), !criteria.Asynchronous) - if err == nil { - if !criteria.Asynchronous { - currentResults := model.NewEventUpdateResults() - err = convertFromElasticUpdateResults(store, response, currentResults) - if err == nil { - mergeElasticUpdateResults(results, currentResults) - } else { - log.WithError(err).WithField("clientHost", store.hostUrls[idx]).Error("Encountered error while updating elasticsearch") - results.Errors = append(results.Errors, err.Error()) - } - } - } else { - log.WithError(err).WithField("clientHost", store.hostUrls[idx]).Error("Encountered error while updating elasticsearch") - results.Errors = append(results.Errors, err.Error()) - } - } - } - - if len(results.Errors) < len(store.esAllClients) { - // Do not fail this request completely since some hosts succeeded. - // The results.Errors property contains the list of errors. - err = nil - } - - results.Complete() - return results, err + store.refreshCache(ctx) + + results := model.NewEventUpdateResults() + results.Criteria = criteria + query, err := convertToElasticUpdateRequest(store, criteria) + if err == nil { + var response string + + for idx, client := range store.esAllClients { + log.WithField("clientHost", store.hostUrls[idx]).Debug("Sending request to client") + response, err = store.updateDocuments(ctx, client, query, store.disableCrossClusterIndexing(strings.Split(store.index, ",")), !criteria.Asynchronous) + if err == nil { + if !criteria.Asynchronous { + currentResults := model.NewEventUpdateResults() + err = convertFromElasticUpdateResults(store, response, currentResults) + if err == nil { + results.AddEventUpdateResults(currentResults) + } else { + log.WithError(err).WithField("clientHost", store.hostUrls[idx]).Error("Encountered error while updating elasticsearch") + results.Errors = append(results.Errors, err.Error()) + } + } + } else { + log.WithError(err).WithField("clientHost", store.hostUrls[idx]).Error("Encountered error while updating elasticsearch") + results.Errors = append(results.Errors, err.Error()) + } + } + } + + if len(results.Errors) < len(store.esAllClients) { + // Do not fail this request completely since some hosts succeeded. + // The results.Errors property contains the list of errors. + err = nil + } + + results.Complete() + return results, err } func (store *ElasticEventstore) luceneSearch(ctx context.Context, query string) (string, error) { - return store.indexSearch(ctx, query, strings.Split(store.index, ",")) + return store.indexSearch(ctx, query, strings.Split(store.index, ",")) } func (store *ElasticEventstore) transformIndex(index string) string { - today := time.Now().Format("2006.01.02") - index = strings.ReplaceAll(index, "{today}", today) - return index + today := time.Now().Format("2006.01.02") + index = strings.ReplaceAll(index, "{today}", today) + return index } func (store *ElasticEventstore) readErrorFromJson(json string) error { - errorType := gjson.Get(json, "error.type").String() - errorReason := gjson.Get(json, "error.reason").String() - errorDetails := json - if len(json) > MAX_ERROR_LENGTH { - errorDetails = json[0:MAX_ERROR_LENGTH] - } - err := errors.New(errorType + ": " + errorReason + " -> " + errorDetails) - return err + errorType := gjson.Get(json, "error.type").String() + errorReason := gjson.Get(json, "error.reason").String() + errorDetails := json + if len(json) > MAX_ERROR_LENGTH { + errorDetails = json[0:MAX_ERROR_LENGTH] + } + err := errors.New(errorType + ": " + errorReason + " -> " + errorDetails) + return err } func (store *ElasticEventstore) readJsonFromResponse(res *esapi.Response) (string, error) { - var err error - var b bytes.Buffer - b.ReadFrom(res.Body) - json := b.String() - if res.IsError() { - err = store.readErrorFromJson(json) - } - return json, err + var err error + var b bytes.Buffer + b.ReadFrom(res.Body) + json := b.String() + if res.IsError() { + err = store.readErrorFromJson(json) + } + return json, err } 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(ctx), - store.esClient.Search.WithIndex(indexes...), - store.esClient.Search.WithBody(strings.NewReader(query)), - store.esClient.Search.WithTrackTotalHits(true), - store.esClient.Search.WithPretty(), - ) - if err == nil { - defer res.Body.Close() - json, err = store.readJsonFromResponse(res) - } - log.WithFields(log.Fields { - "response": json, - "requestId": ctx.Value(web.ContextKeyRequestId), - }).Debug("Search finished") - return json, err + 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(ctx), + store.esClient.Search.WithIndex(indexes...), + store.esClient.Search.WithBody(strings.NewReader(query)), + store.esClient.Search.WithTrackTotalHits(true), + store.esClient.Search.WithPretty(), + ) + if err == nil { + defer res.Body.Close() + json, err = store.readJsonFromResponse(res) + } + log.WithFields(log.Fields{ + "response": json, + "requestId": ctx.Value(web.ContextKeyRequestId), + }).Debug("Search finished") + return json, err } 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")) - - if err != nil { - log.WithError(err).Error("Unable to index acknowledgement into Elasticsearch") - return "", err - } - defer res.Body.Close() - json, err := store.readJsonFromResponse(res) - - log.WithFields(log.Fields{ - "response": json, - "requestId": ctx.Value(web.ContextKeyRequestId), - }).Debug("Index new document finished") - return json, err + 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")) + + if err != nil { + log.WithError(err).Error("Unable to index acknowledgement into Elasticsearch") + return "", err + } + defer res.Body.Close() + json, err := store.readJsonFromResponse(res) + + log.WithFields(log.Fields{ + "response": json, + "requestId": ctx.Value(web.ContextKeyRequestId), + }).Debug("Index new document finished") + return json, err } 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(ctx), - client.UpdateByQuery.WithPretty(), - client.UpdateByQuery.WithConflicts("proceed"), - client.UpdateByQuery.WithBody(strings.NewReader(query)), - client.UpdateByQuery.WithRefresh(true), - client.UpdateByQuery.WithWaitForCompletion(waitForCompletion), - ) - if err == nil { - defer res.Body.Close() - json, err = store.readJsonFromResponse(res) - } - log.WithFields(log.Fields{ - "response": json, - "requestId": ctx.Value(web.ContextKeyRequestId), - }).Debug("Update finished") - return json, err + 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(ctx), + client.UpdateByQuery.WithPretty(), + client.UpdateByQuery.WithConflicts("proceed"), + client.UpdateByQuery.WithBody(strings.NewReader(query)), + client.UpdateByQuery.WithRefresh(true), + client.UpdateByQuery.WithWaitForCompletion(waitForCompletion), + ) + if err == nil { + defer res.Body.Close() + json, err = store.readJsonFromResponse(res) + } + log.WithFields(log.Fields{ + "response": json, + "requestId": ctx.Value(web.ContextKeyRequestId), + }).Debug("Update finished") + return json, err } 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(ctx) - if err == nil { - store.cacheTime = time.Now() - } - } + store.cacheLock.Lock() + defer store.cacheLock.Unlock() + if store.cacheTime.IsZero() || time.Now().Sub(store.cacheTime) > store.cacheMs { + err := store.refreshCacheFromFieldCaps(ctx) + if err == nil { + store.cacheTime = time.Now() + } + } } 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(ctx), - store.esClient.FieldCaps.WithIndex(indexes...), - store.esClient.FieldCaps.WithFields("*"), - store.esClient.FieldCaps.WithPretty(), - ) - if err == nil { - defer res.Body.Close() - json, err = store.readJsonFromResponse(res) - log.WithFields(log.Fields{"response": json}).Debug("Fetch finished") - store.cacheFieldsFromJson(json) - } else { - log.WithError(err).Error("Failed to refresh cache from index patterns") - } - return err + log.Info("Fetching Field Capabilities from Elasticsearch") + indexes := strings.Split(store.index, ",") + var json string + res, err := store.esClient.FieldCaps( + store.esClient.FieldCaps.WithContext(ctx), + store.esClient.FieldCaps.WithIndex(indexes...), + store.esClient.FieldCaps.WithFields("*"), + store.esClient.FieldCaps.WithPretty(), + ) + if err == nil { + defer res.Body.Close() + json, err = store.readJsonFromResponse(res) + log.WithFields(log.Fields{"response": json}).Debug("Fetch finished") + store.cacheFieldsFromJson(json) + } else { + log.WithError(err).Error("Failed to refresh cache from index patterns") + } + return err } func (store *ElasticEventstore) cacheFieldsFromJson(json string) { - store.fieldDefs = make(map[string]*FieldDefinition) - gjson.Get(json, "fields").ForEach(store.cacheFields) + store.fieldDefs = make(map[string]*FieldDefinition) + gjson.Get(json, "fields").ForEach(store.cacheFields) } func (store *ElasticEventstore) cacheFields(name gjson.Result, details gjson.Result) bool { - fieldName := name.String() - detailsMap := make(map[string]map[string]interface{}) - json.NewDecoder(strings.NewReader(details.String())).Decode(&detailsMap) - for _, field := range detailsMap { - fieldType := field["type"].(string) - - fieldDef := &FieldDefinition { - name: fieldName, - fieldType: fieldType, - aggregatable: field["aggregatable"].(bool), - searchable: field["searchable"].(bool), - } - store.fieldDefs[fieldName] = fieldDef - - log.WithFields(log.Fields { - "name": name, - "type": fieldType, - "aggregatable": fieldDef.aggregatable, - "searchable": fieldDef.searchable, - }).Debug("Added field definition") - } - return true + fieldName := name.String() + detailsMap := make(map[string]map[string]interface{}) + json.NewDecoder(strings.NewReader(details.String())).Decode(&detailsMap) + for _, field := range detailsMap { + fieldType := field["type"].(string) + + fieldDef := &FieldDefinition{ + name: fieldName, + fieldType: fieldType, + aggregatable: field["aggregatable"].(bool), + searchable: field["searchable"].(bool), + } + store.fieldDefs[fieldName] = fieldDef + + log.WithFields(log.Fields{ + "name": name, + "type": fieldType, + "aggregatable": fieldDef.aggregatable, + "searchable": fieldDef.searchable, + }).Debug("Added field definition") + } + return true } 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(ctx), - store.esClient.Cluster.State.WithIndex(indexes...), - ) - if err == nil { - defer res.Body.Close() - - var b bytes.Buffer - b.ReadFrom(res.Body) - json = b.String() - - if res.IsError() { - errorType := gjson.Get(json, "error.type").String() - errorReason := gjson.Get(json, "error.reason").String() - errorDetails := json - if len(json) > 255 { - errorDetails = json[0:512] - } - err = errors.New(errorType + ": " + errorReason + " -> " + errorDetails) - } - } - log.WithFields(log.Fields{"response": json}).Debug("Refresh Finished") - return json, err + 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(ctx), + store.esClient.Cluster.State.WithIndex(indexes...), + ) + if err == nil { + defer res.Body.Close() + + var b bytes.Buffer + b.ReadFrom(res.Body) + json = b.String() + + if res.IsError() { + errorType := gjson.Get(json, "error.type").String() + errorReason := gjson.Get(json, "error.reason").String() + errorDetails := json + if len(json) > 255 { + errorDetails = json[0:512] + } + err = errors.New(errorType + ": " + errorReason + " -> " + errorDetails) + } + } + log.WithFields(log.Fields{"response": json}).Debug("Refresh Finished") + return json, err } func (store *ElasticEventstore) parseFirst(json string, name string) string { - result := gjson.Get(json, "hits.hits.0._source." + name).String() - // Select first uid if multiple were provided - if len(result) > 0 && result[0] == '[' { - result = gjson.Get(json, "hits.hits.0._source." + name + ".0").String() - } - return result + result := gjson.Get(json, "hits.hits.0._source."+name).String() + // Select first uid if multiple were provided + if len(result) > 0 && result[0] == '[' { + result = gjson.Get(json, "hits.hits.0._source."+name+".0").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{} + 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] - * - If found, discard original record and replace with the new record - * If the record has source IP/port and destination IP/port, use it as the filter. - * Else if the record has a Zeek x509 "ID" search for the first Zeek record with this ID. - * Else if the record has a Zeek file "FUID" search for the first Zeek record with this FUID. - * Search for the Zeek record with a matching log.id.uid equal to the UID from the previously found record - * - If multiple UIDs exist in the record, use the first UID in the list. - * 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. - */ +* Fetch record via provided Elasticsearch document query. +* If the record has a tunnel_parent, search for a UID=tunnel_parent[0] +* - If found, discard original record and replace with the new record +* If the record has source IP/port and destination IP/port, use it as the filter. +* Else if the record has a Zeek x509 "ID" search for the first Zeek record with this ID. +* Else if the record has a Zeek file "FUID" search for the first Zeek record with this FUID. +* Search for the Zeek record with a matching log.id.uid equal to the UID from the previously found record +* - If multiple UIDs exist in the record, use the first UID in the list. +* 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, idField string, idValue string, timestampStr string, job *model.Job) error { - rangeFilter, timestamp := store.buildRangeFilter(timestampStr) + rangeFilter, timestamp := store.buildRangeFilter(timestampStr) - query := fmt.Sprintf(` + query := fmt.Sprintf(` { "query" : { "bool": { @@ -477,40 +478,40 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(ctx context.Context, idF } }`, idField, idValue, rangeFilter) - var outputSensorId string - filter := model.NewFilter() - 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") - return err - } - - hits := gjson.Get(json, "hits.total.value").Int() - if hits == 0 { - log.WithField("query", query).Error("Pivoted document record was not found") - 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() - if len(tunnelParent) > 0 { - log.Info("Document is inside of a tunnel; attempting to lookup tunnel connection log") - if tunnelParent[0] == '[' { - tunnelParent = gjson.Get(json, "hits.hits.0._source.log.id.tunnel_parents.0").String() - } - query := fmt.Sprintf(` + var outputSensorId string + filter := model.NewFilter() + 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") + return err + } + + hits := gjson.Get(json, "hits.total.value").Int() + if hits == 0 { + log.WithField("query", query).Error("Pivoted document record was not found") + 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() + if len(tunnelParent) > 0 { + log.Info("Document is inside of a tunnel; attempting to lookup tunnel connection log") + if tunnelParent[0] == '[' { + tunnelParent = gjson.Get(json, "hits.hits.0._source.log.id.tunnel_parents.0").String() + } + query := fmt.Sprintf(` { "query" : { "bool": { @@ -521,238 +522,237 @@ func (store *ElasticEventstore) PopulateJobFromDocQuery(ctx context.Context, idF } }`, tunnelParent, rangeFilter) - json, err = store.luceneSearch(ctx, query) - log.WithFields(log.Fields{ - "query": query, - "response": json, - }).Debug("Elasticsearch tunnel search finished") - if err != nil { - log.WithField("query", query).WithError(err).Error("Unable to lookup tunnel record") - return err - } - hits := gjson.Get(json, "hits.total.value").Int() - if hits == 0 { - log.WithField("query", query).Error("Tunnel record was not found") - return errors.New("Unable to locate encapsulating tunnel record") - } - } - - 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()) - filter.DstIp = gjson.Get(json, "hits.hits.0._source.destination.ip").String() - filter.DstPort = int(gjson.Get(json, "hits.hits.0._source.destination.port").Int()) - uid := store.parseFirst(json, "log.id.uid") - x509id := store.parseFirst(json, "log.id.id") - fuid := store.parseFirst(json, "log.id.fuid") - outputSensorId = gjson.Get(json, "hits.hits.0._source.observer.name").String() - duration := int64(store.defaultDurationMs) - - // 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 { - if len(uid) == 0 || uid[0] != 'C' { - zeekFileQuery := "" - if len(x509id) > 0 && x509id[0] == 'F' { - zeekFileQuery = x509id - } else if len(fuid) > 0 && fuid[0] == 'F' { - zeekFileQuery = fuid - } - - 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}}%s]}}}`, - zeekFileQuery, rangeFilter) - 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 - } - - hits = gjson.Get(json, "hits.total.value").Int() - if hits == 0 { - 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") - } - - uid = store.parseFirst(json, "log.id.uid") - } - - if len(uid) == 0 { - 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") - } - } - - // Search for the Zeek connection ID - 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, - "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 - } - - hits = gjson.Get(json, "hits.total.value").Int() - if hits == 0 { - 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") - } - - results := gjson.Get(json, "hits.hits.#._source.\\@timestamp").Array() - var closestDeltaNs int64 - closestDeltaNs = 0 - for idx, ts := range results { - var matchTs time.Time - matchTs, err = time.Parse(time.RFC3339, ts.String()) - if err == nil { - idxStr := strconv.Itoa(idx) - srcIp := gjson.Get(json, "hits.hits." + idxStr + "._source.source.ip").String() - srcPort := int(gjson.Get(json, "hits.hits." + idxStr + "._source.source.port").Int()) - dstIp := gjson.Get(json, "hits.hits." + idxStr + "._source.destination.ip").String() - dstPort := int(gjson.Get(json, "hits.hits." + idxStr + "._source.destination.port").Int()) - - if len(srcIp) > 0 && len(dstIp) > 0 && srcPort > 0 && dstPort > 0 { - delta := timestamp.Sub(matchTs) - deltaNs := delta.Nanoseconds() - if deltaNs < 0 { - deltaNs = -deltaNs - } - if closestDeltaNs == 0 || deltaNs < closestDeltaNs { - closestDeltaNs = deltaNs - - timestamp = matchTs - filter.SrcIp = srcIp - filter.SrcPort = srcPort - filter.DstIp = dstIp - filter.DstPort = dstPort - durationFloat := gjson.Get(json, "hits.hits." + idxStr + "._source.event.duration").Float() - if durationFloat > 0 { - duration = int64(math.Round(durationFloat * 1000.0)) - } - } - } - } - } - - log.WithFields(log.Fields{ - "sensorId": outputSensorId, - "requestId": ctx.Value(web.ContextKeyRequestId), - }).Info("Obtained output parameters") - } - - if len(filter.SrcIp) == 0 || len(filter.DstIp) == 0 || filter.SrcPort == 0 || filter.DstPort == 0 { - 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") - } - - filter.BeginTime = timestamp.Add(time.Duration(-duration - int64(store.timeShiftMs)) * time.Millisecond) - filter.EndTime = timestamp.Add(time.Duration(duration + int64(store.timeShiftMs)) * time.Millisecond) - job.SetNodeId(outputSensorId) - job.Filter = filter - - return nil + json, err = store.luceneSearch(ctx, query) + log.WithFields(log.Fields{ + "query": query, + "response": json, + }).Debug("Elasticsearch tunnel search finished") + if err != nil { + log.WithField("query", query).WithError(err).Error("Unable to lookup tunnel record") + return err + } + hits := gjson.Get(json, "hits.total.value").Int() + if hits == 0 { + log.WithField("query", query).Error("Tunnel record was not found") + return errors.New("Unable to locate encapsulating tunnel record") + } + } + + 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()) + filter.DstIp = gjson.Get(json, "hits.hits.0._source.destination.ip").String() + filter.DstPort = int(gjson.Get(json, "hits.hits.0._source.destination.port").Int()) + uid := store.parseFirst(json, "log.id.uid") + x509id := store.parseFirst(json, "log.id.id") + fuid := store.parseFirst(json, "log.id.fuid") + outputSensorId = gjson.Get(json, "hits.hits.0._source.observer.name").String() + duration := int64(store.defaultDurationMs) + + // 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 { + if len(uid) == 0 || uid[0] != 'C' { + zeekFileQuery := "" + if len(x509id) > 0 && x509id[0] == 'F' { + zeekFileQuery = x509id + } else if len(fuid) > 0 && fuid[0] == 'F' { + zeekFileQuery = fuid + } + + 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}}%s]}}}`, + zeekFileQuery, rangeFilter) + 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 + } + + hits = gjson.Get(json, "hits.total.value").Int() + if hits == 0 { + 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") + } + + uid = store.parseFirst(json, "log.id.uid") + } + + if len(uid) == 0 { + 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") + } + } + + // Search for the Zeek connection ID + 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, + "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 + } + + hits = gjson.Get(json, "hits.total.value").Int() + if hits == 0 { + 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") + } + + results := gjson.Get(json, "hits.hits.#._source.\\@timestamp").Array() + var closestDeltaNs int64 + closestDeltaNs = 0 + for idx, ts := range results { + var matchTs time.Time + matchTs, err = time.Parse(time.RFC3339, ts.String()) + if err == nil { + idxStr := strconv.Itoa(idx) + srcIp := gjson.Get(json, "hits.hits."+idxStr+"._source.source.ip").String() + srcPort := int(gjson.Get(json, "hits.hits."+idxStr+"._source.source.port").Int()) + dstIp := gjson.Get(json, "hits.hits."+idxStr+"._source.destination.ip").String() + dstPort := int(gjson.Get(json, "hits.hits."+idxStr+"._source.destination.port").Int()) + + if len(srcIp) > 0 && len(dstIp) > 0 && srcPort > 0 && dstPort > 0 { + delta := timestamp.Sub(matchTs) + deltaNs := delta.Nanoseconds() + if deltaNs < 0 { + deltaNs = -deltaNs + } + if closestDeltaNs == 0 || deltaNs < closestDeltaNs { + closestDeltaNs = deltaNs + + timestamp = matchTs + filter.SrcIp = srcIp + filter.SrcPort = srcPort + filter.DstIp = dstIp + filter.DstPort = dstPort + durationFloat := gjson.Get(json, "hits.hits."+idxStr+"._source.event.duration").Float() + if durationFloat > 0 { + duration = int64(math.Round(durationFloat * 1000.0)) + } + } + } + } + } + + log.WithFields(log.Fields{ + "sensorId": outputSensorId, + "requestId": ctx.Value(web.ContextKeyRequestId), + }).Info("Obtained output parameters") + } + + if len(filter.SrcIp) == 0 || len(filter.DstIp) == 0 || filter.SrcPort == 0 || filter.DstPort == 0 { + 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") + } + + filter.BeginTime = timestamp.Add(time.Duration(-duration-int64(store.timeShiftMs)) * time.Millisecond) + filter.EndTime = timestamp.Add(time.Duration(duration+int64(store.timeShiftMs)) * time.Millisecond) + job.SetNodeId(outputSensorId) + job.Filter = filter + + return nil } 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 { - log.WithFields(log.Fields { - "searchFilter": ackCriteria.SearchFilter, - "eventFilter": ackCriteria.EventFilter, - "escalate": ackCriteria.Escalate, - "acknowledge": ackCriteria.Acknowledge, - "requestId": ctx.Value(web.ContextKeyRequestId), - }).Info("Acknowledging event") - - updateCriteria := model.NewEventUpdateCriteria() - updateCriteria.AddUpdateScript("ctx._source.event.acknowledged=" + strconv.FormatBool(ackCriteria.Acknowledge)) - if ackCriteria.Escalate && ackCriteria.Acknowledge { - updateCriteria.AddUpdateScript("ctx._source.event.escalated=true") - } - updateCriteria.Populate(ackCriteria.SearchFilter, - ackCriteria.DateRange, - ackCriteria.DateRangeFormat, - ackCriteria.Timezone, - "0", - "0") - - // Add the event filters to the search query - var searchSegment *model.SearchSegment - segment := updateCriteria.ParsedQuery.NamedSegment("search") - if segment == nil { - searchSegment = model.NewSearchSegmentEmpty() - } else { - searchSegment = segment.(*model.SearchSegment) - } - - updateCriteria.Asynchronous = false - for key, value := range ackCriteria.EventFilter { - if (strings.ToLower(key) != "count") { - valueStr := fmt.Sprintf("%v", value) - searchSegment.AddFilter(store.mapElasticField(key), valueStr, model.IsScalar(value), true) - } else if int(value.(float64)) > store.asyncThreshold { - 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 - } - } - - // Baseline the query to be based only on the search component - updateCriteria.ParsedQuery = model.NewQuery() - updateCriteria.ParsedQuery.AddSegment(searchSegment) - - results, err = store.Update(ctx, updateCriteria) - if err == nil && !updateCriteria.Asynchronous { - if results.UpdatedCount == 0 { - if results.UnchangedCount == 0 { - err = errors.New("No eligible events available to acknowledge") - } else { - err = errors.New("All events have already been acknowledged") - } - } - } - } else { - err = errors.New("EventFilter must be specified to ack an event") - } - return results, err + var results *model.EventUpdateResults + var err error + if len(ackCriteria.EventFilter) > 0 { + log.WithFields(log.Fields{ + "searchFilter": ackCriteria.SearchFilter, + "eventFilter": ackCriteria.EventFilter, + "escalate": ackCriteria.Escalate, + "acknowledge": ackCriteria.Acknowledge, + "requestId": ctx.Value(web.ContextKeyRequestId), + }).Info("Acknowledging event") + + updateCriteria := model.NewEventUpdateCriteria() + updateCriteria.AddUpdateScript("ctx._source.event.acknowledged=" + strconv.FormatBool(ackCriteria.Acknowledge)) + if ackCriteria.Escalate && ackCriteria.Acknowledge { + updateCriteria.AddUpdateScript("ctx._source.event.escalated=true") + } + updateCriteria.Populate(ackCriteria.SearchFilter, + ackCriteria.DateRange, + ackCriteria.DateRangeFormat, + ackCriteria.Timezone, + "0", + "0") + + // Add the event filters to the search query + var searchSegment *model.SearchSegment + segment := updateCriteria.ParsedQuery.NamedSegment("search") + if segment == nil { + searchSegment = model.NewSearchSegmentEmpty() + } else { + searchSegment = segment.(*model.SearchSegment) + } + + updateCriteria.Asynchronous = false + for key, value := range ackCriteria.EventFilter { + if strings.ToLower(key) != "count" { + valueStr := fmt.Sprintf("%v", value) + searchSegment.AddFilter(store.mapElasticField(key), valueStr, model.IsScalar(value), true) + } else if int(value.(float64)) > store.asyncThreshold { + 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 + } + } + + // Baseline the query to be based only on the search component + updateCriteria.ParsedQuery = model.NewQuery() + updateCriteria.ParsedQuery.AddSegment(searchSegment) + + results, err = store.Update(ctx, updateCriteria) + if err == nil && !updateCriteria.Asynchronous { + if results.UpdatedCount == 0 { + if results.UnchangedCount == 0 { + err = errors.New("No eligible events available to acknowledge") + } else { + err = errors.New("All events have already been acknowledged") + } + } + } + } else { + err = errors.New("EventFilter must be specified to ack an event") + } + return results, err } - diff --git a/server/modules/elastic/elasticeventstore_test.go b/server/modules/elastic/elasticeventstore_test.go index cc7c5f86..5fb44a79 100644 --- a/server/modules/elastic/elasticeventstore_test.go +++ b/server/modules/elastic/elasticeventstore_test.go @@ -11,142 +11,96 @@ package elastic import ( - "fmt" - "io/ioutil" - "regexp" - "testing" + "fmt" + "io/ioutil" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" ) func TestFieldMapping(tester *testing.T) { - store := &ElasticEventstore{} - - json, err := ioutil.ReadFile("fieldcaps_response.json") - if err != nil { - tester.Errorf("Unexpected error while loading test resource: %v", err) - } - store.cacheFieldsFromJson(string(json)) - - // Exists as keyword and not already aggregatable - actual := store.mapElasticField("smb.service") - if actual != "smb.service.keyword" { - tester.Errorf("expected mapped field %s but got %s", "smb.service.keyword", actual) - } - - // Exists as keyword but already aggregatable - actual = store.mapElasticField("agent.ip") - if actual != "agent.ip" { - tester.Errorf("expected mapped field %s but got %s", "agent.ip", actual) - } - - // Does not exist as valid keyword - actual = store.mapElasticField("event.acknowledged") - if actual != "event.acknowledged" { - tester.Errorf("expected unmapped field %s but got %s", "event.acknowledged", actual) - } - - // Both non-keyword and keyword variants are aggregatable - actual = store.unmapElasticField("agent.ip.keyword") - if actual != "agent.ip.keyword" { - tester.Errorf("expected unmapped field %s but got %s", "agent.ip.keyword", actual) - } - - // Only keyword variant is aggregatable - actual = store.unmapElasticField("smb.service.keyword") - if actual != "smb.service" { - tester.Errorf("expected unmapped field %s but got %s", "smb.service", actual) - } - - // Neither are aggregatable - actual = store.unmapElasticField("event.acknowledged") - if actual != "event.acknowledged" { - tester.Errorf("expected unmapped field %s but got %s", "event.acknowledged", actual) - } + store := &ElasticEventstore{} + + json, err := ioutil.ReadFile("fieldcaps_response.json") + assert.Nil(tester, err) + store.cacheFieldsFromJson(string(json)) + + // Exists as keyword and not already aggregatable + actual := store.mapElasticField("smb.service") + assert.Equal(tester, "smb.service.keyword", actual) + + // Exists as keyword but already aggregatable + actual = store.mapElasticField("agent.ip") + assert.Equal(tester, "agent.ip", actual) + + // Does not exist as valid keyword + actual = store.mapElasticField("event.acknowledged") + assert.Equal(tester, "event.acknowledged", actual) + + // Both non-keyword and keyword variants are aggregatable + actual = store.unmapElasticField("agent.ip.keyword") + assert.Equal(tester, "agent.ip.keyword", actual) + + // Only keyword variant is aggregatable + actual = store.unmapElasticField("smb.service.keyword") + assert.Equal(tester, "smb.service", actual) + + // Neither are aggregatable + actual = store.unmapElasticField("event.acknowledged") + assert.Equal(tester, "event.acknowledged", actual) } func TestFieldMappingCache(tester *testing.T) { - store := &ElasticEventstore{} - - json, err := ioutil.ReadFile("fieldcaps_response.json") - if err != nil { - tester.Errorf("Unexpected error while loading test resource: %v", err) - } - store.cacheFieldsFromJson(string(json)) - - field := store.fieldDefs["smb.service"] - if field == nil { - tester.Errorf("expected field definition") - } - if field.name != "smb.service" { - tester.Errorf("expected name %s but got %s", "ack", field.name) - } - if field.fieldType != "text" { - tester.Errorf("expected fieldType %s but got %s", "text", field.fieldType) - } - if field.aggregatable != false { - tester.Errorf("expected aggregatable %t but got %t", false, field.aggregatable) - } - if field.searchable != true { - tester.Errorf("expected searchable %t but got %t", true, field.searchable) - } - - fieldKeyword := store.fieldDefs["smb.service.keyword"] - if fieldKeyword == nil { - tester.Errorf("expected field definition") - } - if fieldKeyword.name != "smb.service.keyword" { - tester.Errorf("expected name %s but got %s", "smb.service.keyword", fieldKeyword.name) - } - if fieldKeyword.fieldType != "keyword" { - tester.Errorf("expected fieldType %s but got %s", "keyword", fieldKeyword.fieldType) - } - if fieldKeyword.aggregatable != true { - tester.Errorf("expected aggregatable %t but got %t", true, fieldKeyword.aggregatable) - } - if fieldKeyword.searchable != true { - tester.Errorf("expected searchable %t but got %t", true, fieldKeyword.searchable) - } + store := &ElasticEventstore{} + + json, err := ioutil.ReadFile("fieldcaps_response.json") + assert.Nil(tester, err) + store.cacheFieldsFromJson(string(json)) + + field := store.fieldDefs["smb.service"] + if assert.NotNil(tester, field) { + assert.Equal(tester, "smb.service", field.name) + assert.Equal(tester, "text", field.fieldType) + assert.False(tester, field.aggregatable) + assert.True(tester, field.searchable) + } + + fieldKeyword := store.fieldDefs["smb.service.keyword"] + if assert.NotNil(tester, fieldKeyword) { + assert.Equal(tester, "smb.service.keyword", fieldKeyword.name) + assert.Equal(tester, "keyword", fieldKeyword.fieldType) + assert.False(tester, field.aggregatable) + assert.True(tester, field.searchable) + } } func TestTransformIndex(tester *testing.T) { - store := &ElasticEventstore{} - if store.transformIndex("test") != "test" { - tester.Errorf("expected transformed index to be unmodified") - } - - actual := store.transformIndex("test_{today}") - match, _ := regexp.MatchString("test_[0-9]{4}.[0-9]{2}.[0-9]{2}", actual) - if !match { - tester.Errorf("expected transformed index to contain a date") - } + store := &ElasticEventstore{} + assert.Equal(tester, "test", store.transformIndex("test")) + + actual := store.transformIndex("test_{today}") + match, _ := regexp.MatchString("test_[0-9]{4}.[0-9]{2}.[0-9]{2}", actual) + assert.True(tester, match, "expected transformed index to contain a date") } func TestReadErrorFromJson(tester *testing.T) { - store := &ElasticEventstore{} - json := `{"error":{"type":"some type","reason":"some reason"},"something.else":"yes"}` - err := store.readErrorFromJson(json) - if err == nil { - tester.Errorf("Expected error to be returned") - } - expected := `some type: some reason -> {"error":{"type":"some type","reason":"some reason"},"something.else":"yes"}` - actual := fmt.Sprintf("%v", err) - if actual != expected { - tester.Errorf("Expected %s but got %s", expected, actual) - } + store := &ElasticEventstore{} + json := `{"error":{"type":"some type","reason":"some reason"},"something.else":"yes"}` + err := store.readErrorFromJson(json) + assert.Error(tester, err) + expected := `some type: some reason -> {"error":{"type":"some type","reason":"some reason"},"something.else":"yes"}` + actual := fmt.Sprintf("%v", err) + assert.Equal(tester, expected, actual) } func TestDisableCrossClusterIndexing(tester *testing.T) { - store := &ElasticEventstore{} - indexes := make([]string, 2, 2) - indexes[0] = "*:so-*" - indexes[1] = "my-*" - newIndexes := store.disableCrossClusterIndexing(indexes) - if len(newIndexes) != len(indexes) { - tester.Errorf("Expected same array lengths") - } - if newIndexes[0] != "so-*" { - tester.Errorf("Expected disabled cross cluster index but got: %s", newIndexes[0]) - } - if newIndexes[1] != "my-*" { - tester.Errorf("Expected unmodified index but got: %s", newIndexes[1]) - } + store := &ElasticEventstore{} + indexes := make([]string, 2, 2) + indexes[0] = "*:so-*" + indexes[1] = "my-*" + newIndexes := store.disableCrossClusterIndexing(indexes) + assert.Equal(tester, len(indexes), len(newIndexes)) + assert.Equal(tester, "so-*", newIndexes[0]) + assert.Equal(tester, "my-*", newIndexes[1]) } diff --git a/server/modules/elastic/elastictransport_test.go b/server/modules/elastic/elastictransport_test.go index 19b78c8b..bb586e9b 100644 --- a/server/modules/elastic/elastictransport_test.go +++ b/server/modules/elastic/elastictransport_test.go @@ -11,51 +11,47 @@ package elastic import ( - "context" - "net/http" - "testing" - "github.com/security-onion-solutions/securityonion-soc/model" - "github.com/security-onion-solutions/securityonion-soc/web" + "context" + "net/http" + "testing" + + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/web" + "github.com/stretchr/testify/assert" ) type DummyTransport struct { - username string + username string } func (transport *DummyTransport) RoundTrip(req *http.Request) (*http.Response, error) { - transport.username = req.Header.Get("es-security-runas-user") - return nil, nil + 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) - } + 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) + assert.Equal(tester, "test", 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 + 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) + assert.Equal(tester, "mysearchuser", dummy.username) +} diff --git a/server/modules/filedatastore/filedatastoreimpl_test.go b/server/modules/filedatastore/filedatastoreimpl_test.go index 743e7156..6bcd7e6c 100644 --- a/server/modules/filedatastore/filedatastoreimpl_test.go +++ b/server/modules/filedatastore/filedatastoreimpl_test.go @@ -11,130 +11,97 @@ package filedatastore import ( - "os" - "testing" - "github.com/security-onion-solutions/securityonion-soc/module" + "os" + "testing" + + "github.com/security-onion-solutions/securityonion-soc/module" + "github.com/stretchr/testify/assert" ) func TestFileDatastoreInit(tester *testing.T) { - ds := NewFileDatastoreImpl() - cfg := make(module.ModuleConfig) - err := ds.Init(cfg) - if err == nil { - tester.Errorf("expected Init error") - } + ds := NewFileDatastoreImpl() + cfg := make(module.ModuleConfig) + err := ds.Init(cfg) + assert.Error(tester, err) - jobDir := "/tmp/sensoroni.jobs" - cfg["jobDir"] = jobDir - defer os.Remove(jobDir) - os.Mkdir(jobDir, 0777) - err = ds.Init(cfg) - if err != nil { - tester.Errorf("unexpected Init error: %s", err) - } - if ds.retryFailureIntervalMs != DEFAULT_RETRY_FAILURE_INTERVAL_MS { - tester.Errorf("expected retryFailureIntervalMs %d but got %d", DEFAULT_RETRY_FAILURE_INTERVAL_MS, ds.retryFailureIntervalMs) - } + jobDir := "/tmp/sensoroni.jobs" + cfg["jobDir"] = jobDir + defer os.Remove(jobDir) + os.Mkdir(jobDir, 0777) + err = ds.Init(cfg) + if assert.Nil(tester, err) { + assert.Equal(tester, DEFAULT_RETRY_FAILURE_INTERVAL_MS, ds.retryFailureIntervalMs) + } } func TestNodes(tester *testing.T) { - ds := NewFileDatastoreImpl() - cfg := make(module.ModuleConfig) - ds.Init(cfg) - node := ds.CreateNode("foo") - node.Role = "rolo" - node.Description = "desc" - node.Address = "addr" - ds.addNode(node) - nodes := ds.GetNodes() - if len(nodes) != 1 { - tester.Errorf("expected %d nodes but got %d", 1, len(nodes)) - } - if nodes[0].Id != "foo" { - tester.Errorf("expected node.Id %s but got %s", "foo", nodes[0].Id) - } - if nodes[0].Role != "rolo" { - tester.Errorf("expected node.Role %s but got %s", "rolo", nodes[0].Role) - } - if nodes[0].Description != "desc" { - tester.Errorf("expected node.Description %s but got %s", "desc", nodes[0].Description) - } - if nodes[0].Address != "addr" { - tester.Errorf("expected node.Address %s but got %s", "addr", nodes[0].Address) - } - - node = ds.CreateNode("bar") - ds.addNode(node) - nodes = ds.GetNodes() - if len(nodes) != 2 { - tester.Errorf("expected %d nodes but got %d", 2, len(nodes)) - } - job := ds.GetNextJob("foo") - if job != nil { - tester.Errorf("expected no job") - } + ds := NewFileDatastoreImpl() + cfg := make(module.ModuleConfig) + ds.Init(cfg) + node := ds.CreateNode("foo") + node.Role = "rolo" + node.Description = "desc" + node.Address = "addr" + ds.addNode(node) + nodes := ds.GetNodes() + if assert.Len(tester, nodes, 1) { + assert.Equal(tester, "foo", nodes[0].Id) + assert.Equal(tester, "rolo", nodes[0].Role) + assert.Equal(tester, "desc", nodes[0].Description) + assert.Equal(tester, "addr", nodes[0].Address) + } + + node = ds.CreateNode("bar") + ds.addNode(node) + nodes = ds.GetNodes() + assert.Len(tester, nodes, 2) + job := ds.GetNextJob("foo") + assert.Nil(tester, job) } func TestJobs(tester *testing.T) { - ds := NewFileDatastoreImpl() - cfg := make(module.ModuleConfig) - ds.Init(cfg) - node := ds.CreateNode("foo") - ds.addNode(node) + ds := NewFileDatastoreImpl() + cfg := make(module.ModuleConfig) + ds.Init(cfg) + node := ds.CreateNode("foo") + ds.addNode(node) - // Test adding a job - job := ds.CreateJob() - if job.Id != 1001 { - tester.Errorf("expected first job.Id %d but got %d", 1001, job.Id) - } - ds.addJob(job) - job = ds.CreateJob() - if job.Id != 1002 { - tester.Errorf("expected second job.Id %d but got %d", 1002, job.Id) - } - ds.addJob(job) + // Test adding a job + job := ds.CreateJob() + assert.Equal(tester, 1001, job.Id) + ds.addJob(job) + job = ds.CreateJob() + assert.Equal(tester, 1002, job.Id) + ds.addJob(job) - // Test fetching a job - job = ds.getJobById(1001) - if job.Id != 1001 { - tester.Errorf("expected getJobById job.Id %d but got %d", 1001, job.Id) - } + // Test fetching a job + job = ds.getJobById(1001) + assert.Equal(tester, 1001, job.Id) - job = ds.GetJob(1002) - if job.Id != 1002 { - tester.Errorf("expected GetJob job.Id %d but got %d", 1002, job.Id) - } - job = ds.GetJob(1003) - if job != nil { - tester.Errorf("expected nil GetJob job.Id but got %d", job.Id) - } + job = ds.GetJob(1002) + assert.Equal(tester, 1002, job.Id) - // Test fetching all jobs - jobs := ds.GetJobs() - if len(jobs) != 2 { - tester.Errorf("expected GetJobs array size to be %d but got %d", 2, len(jobs)) - } + job = ds.GetJob(1003) + assert.Nil(tester, job) - // Test deleting jobs - ds.deleteJob(jobs[0]) - jobs = ds.GetJobs() - if len(jobs) != 1 { - tester.Errorf("expected post-delete GetJobs array size to be %d but got %d", 1, len(jobs)) - } - ds.deleteJob(jobs[0]) - jobs = ds.GetJobs() - if len(jobs) != 0 { - tester.Errorf("expected post-delete GetJobs array size to be %d but got %d", 0, len(jobs)) - } + // Test fetching all jobs + jobs := ds.GetJobs() + assert.Len(tester, jobs, 2) + + // Test deleting jobs + ds.deleteJob(jobs[0]) + jobs = ds.GetJobs() + assert.Len(tester, jobs, 1) + ds.deleteJob(jobs[0]) + jobs = ds.GetJobs() + assert.Len(tester, jobs, 0) } func TestGetStreamFilename(tester *testing.T) { - ds := NewFileDatastoreImpl() - cfg := make(module.ModuleConfig) - cfg["jobDir"] = "/tmp/jobs" - ds.Init(cfg) - filename := ds.getStreamFilename(ds.CreateJob()) - if filename != "/tmp/jobs/1001.bin" { - tester.Errorf("expected job filename %s but got %s", "/tmp/jobs/1001.bin", filename) - } -} \ No newline at end of file + ds := NewFileDatastoreImpl() + cfg := make(module.ModuleConfig) + cfg["jobDir"] = "/tmp/jobs" + ds.Init(cfg) + filename := ds.getStreamFilename(ds.CreateJob()) + assert.Equal(tester, "/tmp/jobs/1001.bin", filename) +} diff --git a/server/modules/influxdb/influxdb_test.go b/server/modules/influxdb/influxdb_test.go index 8a54dab6..8c4ab940 100644 --- a/server/modules/influxdb/influxdb_test.go +++ b/server/modules/influxdb/influxdb_test.go @@ -10,91 +10,64 @@ package influxdb import ( - "testing" - "github.com/security-onion-solutions/securityonion-soc/module" + "testing" + + "github.com/security-onion-solutions/securityonion-soc/module" + "github.com/stretchr/testify/assert" ) func TestInfluxDBInit(tester *testing.T) { - influxdb := NewInfluxDB(nil) - cfg := make(module.ModuleConfig) - cfg["hostUrl"] = "http://some.where" - cfg["org"] = "testorg" - cfg["bucket"] = "testbucket" - cfg["cacheExpirationMs"] = float64(12345) - err := influxdb.Init(cfg) - if err != nil { - tester.Errorf("unexpected Init error: %s", err) - } - if influxdb.metrics.org != "testorg" { - tester.Errorf("Expected testorg") - } - if influxdb.metrics.bucket != "testbucket" { - tester.Errorf("Expected testbucket") - } - if influxdb.metrics.cacheExpirationMs != 12345 { - tester.Errorf("Expected cacheExpirationMs to be overriden") - } - if influxdb.metrics.queryApi == nil { - tester.Errorf("Expected constructed queryApi") - } + influxdb := NewInfluxDB(nil) + cfg := make(module.ModuleConfig) + cfg["hostUrl"] = "http://some.where" + cfg["org"] = "testorg" + cfg["bucket"] = "testbucket" + cfg["cacheExpirationMs"] = float64(12345) + err := influxdb.Init(cfg) + if assert.Nil(tester, err) { + assert.Equal(tester, cfg["org"], influxdb.metrics.org) + assert.Equal(tester, cfg["bucket"], influxdb.metrics.bucket) + excpectedCacheExpirationMs := int(cfg["cacheExpirationMs"].(float64)) + assert.Equal(tester, excpectedCacheExpirationMs, influxdb.metrics.cacheExpirationMs) + assert.NotNil(tester, influxdb.metrics.queryApi) + } } func TestInfluxDBInitNoUrl(tester *testing.T) { - influxdb := NewInfluxDB(nil) - cfg := make(module.ModuleConfig) - cfg["org"] = "testorg" - cfg["bucket"] = "testbucket" - cfg["cacheExpirationMs"] = float64(12345) - err := influxdb.Init(cfg) - if err != nil { - tester.Errorf("unexpected Init error: %s", err) - } - if influxdb.metrics.org != "testorg" { - tester.Errorf("Expected testorg") - } - if influxdb.metrics.bucket != "testbucket" { - tester.Errorf("Expected testbucket") - } - if influxdb.metrics.cacheExpirationMs != 12345 { - tester.Errorf("Expected cacheExpirationMs to be overriden") - } - if influxdb.metrics.queryApi != nil { - tester.Errorf("Expected unconstructed queryApi") - } + influxdb := NewInfluxDB(nil) + cfg := make(module.ModuleConfig) + cfg["org"] = "testorg" + cfg["bucket"] = "testbucket" + cfg["cacheExpirationMs"] = float64(12345) + err := influxdb.Init(cfg) + if assert.Nil(tester, err) { + assert.Equal(tester, cfg["org"], influxdb.metrics.org) + assert.Equal(tester, cfg["bucket"], influxdb.metrics.bucket) + excpectedCacheExpirationMs := int(cfg["cacheExpirationMs"].(float64)) + assert.Equal(tester, excpectedCacheExpirationMs, influxdb.metrics.cacheExpirationMs) + assert.Nil(tester, influxdb.metrics.queryApi) + } } func TestInfluxDBInitDefaults(tester *testing.T) { - influxdb := NewInfluxDB(nil) - cfg := make(module.ModuleConfig) - cfg["hostUrl"] = "http://some.where" - err := influxdb.Init(cfg) - if err != nil { - tester.Errorf("unexpected Init error: %s", err) - } - if influxdb.metrics.org != DEFAULT_ORG { - tester.Errorf("Expected default org") - } - if influxdb.metrics.bucket != DEFAULT_BUCKET { - tester.Errorf("Expected default bucket but got %s", influxdb.metrics.bucket) - } - if influxdb.metrics.cacheExpirationMs != DEFAULT_CACHE_EXPIRATION_MS { - tester.Errorf("Expected default cacheExpirationMs") - } - if influxdb.metrics.queryApi == nil { - tester.Errorf("Expected constructed queryApi") - } + influxdb := NewInfluxDB(nil) + cfg := make(module.ModuleConfig) + cfg["hostUrl"] = "http://some.where" + err := influxdb.Init(cfg) + if assert.Nil(tester, err) { + assert.Equal(tester, DEFAULT_ORG, influxdb.metrics.org) + assert.Equal(tester, DEFAULT_BUCKET, influxdb.metrics.bucket) + assert.Equal(tester, DEFAULT_CACHE_EXPIRATION_MS, influxdb.metrics.cacheExpirationMs) + assert.NotNil(tester, influxdb.metrics.queryApi) + } } func TestInfluxDBStop(tester *testing.T) { - influxdb := NewInfluxDB(nil) - cfg := make(module.ModuleConfig) - cfg["hostUrl"] = "http://some.where" - influxdb.Init(cfg) - if !influxdb.IsRunning() { - tester.Errorf("Expected IsRunning = true") - } - influxdb.Stop() - if influxdb.IsRunning() { - tester.Errorf("Expected IsRunning = false") - } + influxdb := NewInfluxDB(nil) + cfg := make(module.ModuleConfig) + cfg["hostUrl"] = "http://some.where" + influxdb.Init(cfg) + assert.True(tester, influxdb.IsRunning()) + influxdb.Stop() + assert.False(tester, influxdb.IsRunning()) } diff --git a/server/modules/influxdb/influxdbmetrics_test.go b/server/modules/influxdb/influxdbmetrics_test.go index 0935ccb2..b5b3553e 100644 --- a/server/modules/influxdb/influxdbmetrics_test.go +++ b/server/modules/influxdb/influxdbmetrics_test.go @@ -10,35 +10,32 @@ package influxdb import ( - "testing" - "time" - "github.com/security-onion-solutions/securityonion-soc/model" + "testing" + "time" + + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/stretchr/testify/assert" ) func TestConvertValuesToString(tester *testing.T) { metrics := NewInfluxDBMetrics() - values := make(map[string]interface{}) - values["foo"] = "bar" - values["bar"] = 1 - strValues := metrics.convertValuesToString(values) - if strValues["foo"] != "bar" { - tester.Errorf("Expected bar string but got %v", strValues["foo"]) - } + values := make(map[string]interface{}) + values["foo"] = "bar" + values["bar"] = 1 + strValues := metrics.convertValuesToString(values) + assert.Equal(tester, values["foo"], strValues["foo"]) } func TestConvertValuesToInt(tester *testing.T) { - metrics := NewInfluxDBMetrics() - values := make(map[string]interface{}) - values["foo"] = 1234 - values["bar"] = 9876.1 - values["zoo"] = "garbage" - intValues := metrics.convertValuesToInt(values) - if intValues["foo"] != 1234 { - tester.Errorf("Expected 1234 int but got %v", intValues["foo"]) - } - if intValues["bar"] != 9876 { - tester.Errorf("Expected 9876 int but got %v", intValues["bar"]) - } + metrics := NewInfluxDBMetrics() + values := make(map[string]interface{}) + values["foo"] = 1234 + values["bar"] = 9876.1 + values["zoo"] = "garbage" + intValues := metrics.convertValuesToInt(values) + assert.Equal(tester, values["foo"], intValues["foo"]) + expectedBarVal := int(values["bar"].(float64)) + assert.Equal(tester, expectedBarVal, intValues["bar"], "float64 should be cast to int") } func TestGetRaidStatus(tester *testing.T) { @@ -47,15 +44,9 @@ func TestGetRaidStatus(tester *testing.T) { metrics.raidStatus["foo"] = 0 metrics.raidStatus["bar"] = 1 - if metrics.getRaidStatus("foo") != model.NodeStatusOk { - tester.Errorf("Expected ok status but got %s", metrics.getRaidStatus("foo")) - } - if metrics.getRaidStatus("bar") != model.NodeStatusFault { - tester.Errorf("Expected fault status but got %s", metrics.getRaidStatus("foo")) - } - if metrics.getRaidStatus("no") != model.NodeStatusUnknown { - tester.Errorf("Expected unknown status but got %s", metrics.getRaidStatus("foo")) - } + assert.Equal(tester, model.NodeStatusOk, metrics.getRaidStatus("foo")) + assert.Equal(tester, model.NodeStatusFault, metrics.getRaidStatus("bar")) + assert.Equal(tester, model.NodeStatusUnknown, metrics.getRaidStatus("missing")) } func TestGetProcessStatus(tester *testing.T) { @@ -64,15 +55,9 @@ func TestGetProcessStatus(tester *testing.T) { metrics.processStatus["foo"] = 0 metrics.processStatus["bar"] = 1 - if metrics.getProcessStatus("foo") != model.NodeStatusOk { - tester.Errorf("Expected ok status but got %s", metrics.getProcessStatus("foo")) - } - if metrics.getProcessStatus("bar") != model.NodeStatusFault { - tester.Errorf("Expected fault status but got %s", metrics.getProcessStatus("foo")) - } - if metrics.getProcessStatus("no") != model.NodeStatusUnknown { - tester.Errorf("Expected unknown status but got %s", metrics.getProcessStatus("foo")) - } + assert.Equal(tester, model.NodeStatusOk, metrics.getProcessStatus("foo")) + assert.Equal(tester, model.NodeStatusFault, metrics.getProcessStatus("bar")) + assert.Equal(tester, model.NodeStatusUnknown, metrics.getProcessStatus("missing")) } func TestGetProductionEps(tester *testing.T) { @@ -82,15 +67,10 @@ func TestGetProductionEps(tester *testing.T) { metrics.productionEps["bar"] = 1 metrics.productionEps["zoo"] = 2 - if metrics.getProductionEps("foo") != 0 { - tester.Errorf("Expected 0 but got %d", metrics.getProductionEps("foo")) - } - if metrics.getProductionEps("bar") != 1 { - tester.Errorf("Expected 1 but got %d", metrics.getProductionEps("bar")) - } - if metrics.getProductionEps("zoo") != 2 { - tester.Errorf("Expected 2 but got %d", metrics.getProductionEps("zoo")) - } + assert.Equal(tester, 0, metrics.getProductionEps("foo")) + assert.Equal(tester, 1, metrics.getProductionEps("bar")) + assert.Equal(tester, 2, metrics.getProductionEps("zoo")) + assert.Equal(tester, 0, metrics.getProductionEps("missing")) } func TestGetConsumptionEps(tester *testing.T) { @@ -100,18 +80,11 @@ func TestGetConsumptionEps(tester *testing.T) { metrics.consumptionEps["bar"] = 1 metrics.consumptionEps["zoo"] = 2 - if metrics.getConsumptionEps("foo") != 0 { - tester.Errorf("Expected 0 but got %d", metrics.getConsumptionEps("foo")) - } - if metrics.getConsumptionEps("bar") != 1 { - tester.Errorf("Expected 1 but got %d", metrics.getConsumptionEps("bar")) - } - if metrics.getConsumptionEps("zoo") != 2 { - tester.Errorf("Expected 2 but got %d", metrics.getConsumptionEps("zoo")) - } - if metrics.GetGridEps() != 3 { - tester.Errorf("Expected 3 but got %d", metrics.GetGridEps()) - } + assert.Equal(tester, 0, metrics.getConsumptionEps("foo")) + assert.Equal(tester, 1, metrics.getConsumptionEps("bar")) + assert.Equal(tester, 2, metrics.getConsumptionEps("zoo")) + assert.Equal(tester, 0, metrics.getConsumptionEps("missing")) + assert.Equal(tester, 3, metrics.GetGridEps()) } func TestGetFailedEvents(tester *testing.T) { @@ -121,13 +94,8 @@ func TestGetFailedEvents(tester *testing.T) { metrics.failedEvents["bar"] = 1 metrics.failedEvents["zoo"] = 2 - if metrics.getFailedEvents("foo") != 0 { - tester.Errorf("Expected 0 but got %d", metrics.getFailedEvents("foo")) - } - if metrics.getFailedEvents("bar") != 1 { - tester.Errorf("Expected 1 but got %d", metrics.getFailedEvents("bar")) - } - if metrics.getFailedEvents("zoo") != 2 { - tester.Errorf("Expected 2 but got %d", metrics.getFailedEvents("zoo")) - } + assert.Equal(tester, 0, metrics.getFailedEvents("foo")) + assert.Equal(tester, 1, metrics.getFailedEvents("bar")) + assert.Equal(tester, 2, metrics.getFailedEvents("zoo")) + assert.Equal(tester, 0, metrics.getFailedEvents("missing")) } diff --git a/server/modules/kratos/kratos_test.go b/server/modules/kratos/kratos_test.go index b1749b6a..71226324 100644 --- a/server/modules/kratos/kratos_test.go +++ b/server/modules/kratos/kratos_test.go @@ -10,28 +10,25 @@ package kratos import ( - "testing" - "github.com/security-onion-solutions/securityonion-soc/config" - "github.com/security-onion-solutions/securityonion-soc/module" - "github.com/security-onion-solutions/securityonion-soc/server" + "testing" + + "github.com/security-onion-solutions/securityonion-soc/config" + "github.com/security-onion-solutions/securityonion-soc/module" + "github.com/security-onion-solutions/securityonion-soc/server" + "github.com/stretchr/testify/assert" ) func TestInit(tester *testing.T) { - scfg := &config.ServerConfig{} - srv := server.NewServer(scfg, "") - kratos := NewKratos(srv) - cfg := make(module.ModuleConfig) - err := kratos.Init(cfg) - if err == nil { - tester.Errorf("expected Init error") - } + scfg := &config.ServerConfig{} + srv := server.NewServer(scfg, "") + kratos := NewKratos(srv) + cfg := make(module.ModuleConfig) + err := kratos.Init(cfg) + assert.Error(tester, err) - cfg["hostUrl"] = "abc" - err = kratos.Init(cfg) - if err != nil { - tester.Errorf("unexpected Init error") - } - if kratos.server.Userstore == nil { - tester.Errorf("expected non-nil Userstore") - } + cfg["hostUrl"] = "abc" + err = kratos.Init(cfg) + if assert.Nil(tester, err) { + assert.NotNil(tester, kratos.server.Userstore) + } } diff --git a/server/modules/kratos/kratospreprocessor_test.go b/server/modules/kratos/kratospreprocessor_test.go index 19369ee4..0239f818 100644 --- a/server/modules/kratos/kratospreprocessor_test.go +++ b/server/modules/kratos/kratospreprocessor_test.go @@ -10,28 +10,28 @@ package kratos import ( - "context" - "github.com/security-onion-solutions/securityonion-soc/fake" - "github.com/security-onion-solutions/securityonion-soc/model" - "github.com/security-onion-solutions/securityonion-soc/web" - "net/http" - "testing" + "context" + "net/http" + "testing" + + "github.com/security-onion-solutions/securityonion-soc/fake" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/web" + "github.com/stretchr/testify/assert" ) func TestPreprocessPriority(tester *testing.T) { - handler := NewKratosPreprocessor(nil) - if handler.PreprocessPriority() != 110 { - tester.Error("expected 110 priority") - } + handler := NewKratosPreprocessor(nil) + assert.Equal(tester, 110, handler.PreprocessPriority()) } func TestPreprocess(tester *testing.T) { - expectedId := "112233" + expectedId := "112233" - user := model.NewUser() - user.Id = expectedId - userstore := NewKratosUserstore(fake.NewAuthorizedServer(make(map[string][]string))) - userstore.Init("some/url") - kratosUsersResponseJson := ` + user := model.NewUser() + user.Id = expectedId + userstore := NewKratosUserstore(fake.NewAuthorizedServer(make(map[string][]string))) + userstore.Init("some/url") + kratosUsersResponseJson := ` [ { "credentials": {}, @@ -46,38 +46,27 @@ func TestPreprocess(tester *testing.T) { "verifiable_addresses": [] } ]` - userstore.client.MockStringResponse(kratosUsersResponseJson, 200, nil) + userstore.client.MockStringResponse(kratosUsersResponseJson, 200, nil) + + handler := NewKratosPreprocessor(userstore) + request, _ := http.NewRequest("GET", "", nil) - handler := NewKratosPreprocessor(userstore) - request, _ := http.NewRequest("GET", "", nil) + request.Header.Set("x-user-id", expectedId) - request.Header.Set("x-user-id", expectedId) + ctx, statusCode, err := handler.Preprocess(context.Background(), request) + if assert.Nil(tester, err) { + assert.Zero(tester, statusCode) + assert.NotNil(tester, ctx) + } - 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) + assert.NotNil(tester, requestor) - 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) - } + actualId := requestor.(*model.User).Id + assert.Equal(tester, expectedId, actualId) - requestorId := ctx.Value(web.ContextKeyRequestorId) - if requestorId == nil { - tester.Errorf("Expected non-nil requestor ID") - } - if requestorId != expectedId { - tester.Errorf("expected %s but got %s", expectedId, requestorId) - } + requestorId := ctx.Value(web.ContextKeyRequestorId) + if assert.NotNil(tester, requestorId) { + assert.Equal(tester, expectedId, requestorId) + } } diff --git a/server/modules/kratos/kratosuser_test.go b/server/modules/kratos/kratosuser_test.go index 0fc54764..8ae8d5d4 100644 --- a/server/modules/kratos/kratosuser_test.go +++ b/server/modules/kratos/kratosuser_test.go @@ -10,52 +10,34 @@ package kratos import ( - "github.com/security-onion-solutions/securityonion-soc/model" - "testing" + "testing" + + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/stretchr/testify/assert" ) func TestCopyFromUser(tester *testing.T) { - kratosUser := &KratosUser{} - user := model.NewUser() - user.Email = "my@email" - user.FirstName = "myFirstname" - user.LastName = "myLastname" - user.Status = "locked" - kratosUser.copyFromUser(user) - if kratosUser.Traits.Email != user.Email { - tester.Errorf("Email failed to convert") - } - if kratosUser.Traits.FirstName != user.FirstName { - tester.Errorf("FirstName failed to convert") - } - if kratosUser.Traits.LastName != user.LastName { - tester.Errorf("LastName failed to convert") - } - if kratosUser.Traits.Status != user.Status { - tester.Errorf("Status failed to convert") - } - if kratosUser.Addresses[0].Value != user.Email { - tester.Errorf("Address failed to convert") - } + kratosUser := &KratosUser{} + user := model.NewUser() + user.Email = "my@email" + user.FirstName = "myFirstname" + user.LastName = "myLastname" + user.Status = "locked" + kratosUser.copyFromUser(user) + assert.Equal(tester, user.Email, kratosUser.Traits.Email) + assert.Equal(tester, user.FirstName, kratosUser.Traits.FirstName) + assert.Equal(tester, user.LastName, kratosUser.Traits.LastName) + assert.Equal(tester, user.Status, kratosUser.Traits.Status) + assert.Equal(tester, user.Email, kratosUser.Addresses[0].Value) } func TestCopyToUser(tester *testing.T) { - kratosUser := NewKratosUser("myEmail", "myFirst", "myLast", "locked") - user := model.NewUser() - kratosUser.copyToUser(user) - if kratosUser.Traits.Email != user.Email { - tester.Errorf("Email failed to convert") - } - if kratosUser.Traits.FirstName != user.FirstName { - tester.Errorf("FirstName failed to convert") - } - if kratosUser.Traits.LastName != user.LastName { - tester.Errorf("LastName failed to convert") - } - if kratosUser.Traits.Status != user.Status { - tester.Errorf("Status failed to convert") - } - if kratosUser.Addresses[0].Value != user.Email { - tester.Errorf("Address failed to convert") - } + kratosUser := NewKratosUser("myEmail", "myFirst", "myLast", "locked") + user := model.NewUser() + kratosUser.copyToUser(user) + assert.Equal(tester, kratosUser.Traits.Email, user.Email) + assert.Equal(tester, kratosUser.Traits.FirstName, user.FirstName) + assert.Equal(tester, kratosUser.Traits.LastName, user.LastName) + assert.Equal(tester, kratosUser.Traits.Status, user.Status) + assert.Equal(tester, kratosUser.Addresses[0].Value, user.Email) } diff --git a/server/modules/kratos/kratosuserstore_test.go b/server/modules/kratos/kratosuserstore_test.go index d45ea32c..b806220f 100644 --- a/server/modules/kratos/kratosuserstore_test.go +++ b/server/modules/kratos/kratosuserstore_test.go @@ -10,34 +10,31 @@ package kratos import ( - "context" - "errors" - "github.com/security-onion-solutions/securityonion-soc/fake" - "github.com/security-onion-solutions/securityonion-soc/model" - "testing" + "context" + "testing" + + "github.com/security-onion-solutions/securityonion-soc/fake" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/stretchr/testify/assert" ) func TestUserstoreInit(tester *testing.T) { - ai := NewKratosUserstore(nil) - err := ai.Init("abc") - if err != nil { - tester.Errorf("unexpected Init error") - } + ai := NewKratosUserstore(nil) + err := ai.Init("abc") + assert.Nil(tester, err) } func TestUnauthorized(tester *testing.T) { - userStore := NewKratosUserstore(fake.NewUnauthorizedServer()) + userStore := NewKratosUserstore(fake.NewUnauthorizedServer()) - _, err := userStore.GetUsers(context.Background()) - ensureUnauthorized(tester, err) + _, err := userStore.GetUsers(context.Background()) + ensureUnauthorized(tester, err) - _, err = userStore.GetUser(context.Background(), "some-id") - ensureUnauthorized(tester, err) + _, err = userStore.GetUser(context.Background(), "some-id") + ensureUnauthorized(tester, err) } func ensureUnauthorized(tester *testing.T, err error) { - var authErr *model.Unauthorized - if err == nil || !errors.As(err, &authErr) { - tester.Errorf("Expected unauthorized error but got %v", err) - } + var authErr *model.Unauthorized + assert.ErrorAs(tester, err, &authErr) } diff --git a/server/modules/modules_test.go b/server/modules/modules_test.go index 49f99681..0a05e672 100644 --- a/server/modules/modules_test.go +++ b/server/modules/modules_test.go @@ -11,23 +11,24 @@ package modules import ( - "github.com/security-onion-solutions/securityonion-soc/module" - "testing" + "testing" + + "github.com/security-onion-solutions/securityonion-soc/module" + "github.com/stretchr/testify/assert" ) func TestBuildModuleMap(tester *testing.T) { - mm := BuildModuleMap(nil) - findModule(tester, mm, "elastic") - findModule(tester, mm, "filedatastore") - findModule(tester, mm, "kratos") - findModule(tester, mm, "influxdb") - findModule(tester, mm, "sostatus") - findModule(tester, mm, "statickeyauth") - findModule(tester, mm, "thehive") + mm := BuildModuleMap(nil) + findModule(tester, mm, "elastic") + findModule(tester, mm, "filedatastore") + findModule(tester, mm, "kratos") + findModule(tester, mm, "influxdb") + findModule(tester, mm, "sostatus") + findModule(tester, mm, "statickeyauth") + findModule(tester, mm, "thehive") } func findModule(tester *testing.T, mm map[string]module.Module, module string) { - if _, ok := mm[module]; !ok { - tester.Errorf("missing module %s", module) - } + _, ok := mm[module] + assert.True(tester, ok) } diff --git a/server/modules/sostatus/sostatus_test.go b/server/modules/sostatus/sostatus_test.go index 023cc030..f53677d7 100644 --- a/server/modules/sostatus/sostatus_test.go +++ b/server/modules/sostatus/sostatus_test.go @@ -10,22 +10,19 @@ package sostatus import ( - "testing" + "testing" + + "github.com/stretchr/testify/assert" ) func TestSoStatusInit(tester *testing.T) { - status := NewSoStatus(nil) - cfg := make(map[string]interface{}) - cfg["refreshIntervalMs"] = float64(1000) - cfg["offlineThresholdMs"] = float64(2000) - err := status.Init(cfg) - if err != nil { - tester.Errorf("unexpected Init error") - } - if status.refreshIntervalMs != 1000 { - tester.Errorf("Unexpected refresh interval value %d", status.refreshIntervalMs) - } - if status.offlineThresholdMs != 2000 { - tester.Errorf("Unexpected threshold value %d", status.offlineThresholdMs) - } + status := NewSoStatus(nil) + cfg := make(map[string]interface{}) + cfg["refreshIntervalMs"] = float64(1000) + cfg["offlineThresholdMs"] = float64(2000) + err := status.Init(cfg) + if assert.Nil(tester, err) { + assert.Equal(tester, 1000, status.refreshIntervalMs) + assert.Equal(tester, 2000, status.offlineThresholdMs) + } } diff --git a/server/modules/statickeyauth/statickeyauth_test.go b/server/modules/statickeyauth/statickeyauth_test.go index 0d189850..47b831e4 100644 --- a/server/modules/statickeyauth/statickeyauth_test.go +++ b/server/modules/statickeyauth/statickeyauth_test.go @@ -16,6 +16,7 @@ import ( "github.com/security-onion-solutions/securityonion-soc/config" "github.com/security-onion-solutions/securityonion-soc/module" "github.com/security-onion-solutions/securityonion-soc/server" + "github.com/stretchr/testify/assert" ) func TestAuthInit(tester *testing.T) { @@ -35,22 +36,14 @@ 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") - } + assert.Len(tester, auth.server.Host.Preprocessors(), 1) err := auth.Init(cfg) if failure { - if err == nil { - tester.Errorf("expected Init error") - } - } else if err != nil { - tester.Errorf("Unexpacted error: %v", err) + assert.Error(tester, err, "Expected Init error") } else { - if auth.impl.anonymousNetwork.String() != expectedCidr { - tester.Errorf("expected anonymousNetwork %s but got %s", expectedCidr, auth.impl.anonymousNetwork.String()) - } - if len(auth.server.Host.Preprocessors()) != 2 { - tester.Errorf("expected two preprocessors to now exist") + if assert.Nil(tester, err) { + assert.Equal(tester, expectedCidr, auth.impl.anonymousNetwork.String()) + assert.Len(tester, auth.server.Host.Preprocessors(), 2) } } } diff --git a/server/modules/statickeyauth/statickeyauthimpl_test.go b/server/modules/statickeyauth/statickeyauthimpl_test.go index 71b40ab8..aece97aa 100644 --- a/server/modules/statickeyauth/statickeyauthimpl_test.go +++ b/server/modules/statickeyauth/statickeyauthimpl_test.go @@ -14,7 +14,9 @@ import ( "context" "net/http" "testing" + "github.com/security-onion-solutions/securityonion-soc/web" + "github.com/stretchr/testify/assert" ) func TestValidateAuthorization(tester *testing.T) { @@ -30,9 +32,7 @@ func validateAuthorization(tester *testing.T, key string, ip string, expected bo ai := NewStaticKeyAuthImpl() ai.Init("abc", "172.17.0.0/24") actual := ai.validateAuthorization(key, ip) - if actual != expected { - tester.Errorf("expected authorization [key=%s, ip=%s] result %t but got %t", key, ip, expected, actual) - } + assert.Equal(tester, expected, actual) } func TestValidateApiKey(tester *testing.T) { @@ -47,58 +47,42 @@ func validateKey(tester *testing.T, key string, expected bool) { ai := NewStaticKeyAuthImpl() ai.apiKey = "abc" actual := ai.validateApiKey(key) - if actual != expected { - tester.Errorf("expected validateApiKey %t but got %t", expected, actual) - } + assert.Equal(tester, expected, actual) } func TestAuthImplInit(tester *testing.T) { ai := NewStaticKeyAuthImpl() err := ai.Init("abc", "1") - if err == nil { - tester.Errorf("expected Init error") - } + assert.Error(tester, err) err = ai.Init("abc", "1.2.3.4/16") - if err != nil { - tester.Errorf("unexpected Init error") - } - if ai.apiKey != "abc" { - tester.Errorf("expected apiKey %s but got %s", "abc", ai.apiKey) - } - if ai.anonymousNetwork.String() != "1.2.0.0/16" { - tester.Errorf("expected anonymousNetwork %s but got %s", "1.2.3.4/16", ai.anonymousNetwork.String()) + if assert.Nil(tester, err) { + assert.Equal(tester, "abc", ai.apiKey) + assert.Equal(tester, "1.2.0.0/16", ai.anonymousNetwork.String()) } } func TestPreprocessPriority(tester *testing.T) { - handler := NewStaticKeyAuthImpl() - if handler.PreprocessPriority() != 100 { - tester.Error("expected 100 priority") - } + handler := NewStaticKeyAuthImpl() + assert.Equal(tester, 100, handler.PreprocessPriority()) } func TestPreprocess(tester *testing.T) { ai := NewStaticKeyAuthImpl() err := ai.Init("abc", "1") + assert.Error(tester, err) 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 + ctx, statusCode, err := ai.Preprocess(context.Background(), request) + if assert.Nil(tester, err) { + assert.Zero(tester, statusCode) + if assert.NotNil(tester, ctx) { + requestor := ctx.Value(web.ContextKeyRequestor) + if assert.NotNil(tester, requestor) { + actualId := requestor.(string) + assert.Equal(tester, "SONODE", actualId) + } + } + } + +} diff --git a/web/websockethandler_test.go b/web/websockethandler_test.go index ded03b3b..76534ed4 100644 --- a/web/websockethandler_test.go +++ b/web/websockethandler_test.go @@ -11,8 +11,10 @@ package web import ( - "testing" - "time" + "testing" + "time" + + "github.com/stretchr/testify/assert" ) func TestHandlePingMessage(tester *testing.T) { @@ -20,11 +22,8 @@ func TestHandlePingMessage(tester *testing.T) { conn := NewConnection(nil, "") oldPingTime := conn.lastPingTime time.Sleep(3 * time.Millisecond) - msg := &WebSocketMessage{ Kind: "Ping" } + msg := &WebSocketMessage{Kind: "Ping"} webSocketHandler.handleMessage(msg, conn) newPingTime := conn.lastPingTime - - if newPingTime.Sub(oldPingTime).Milliseconds() < 3 { - tester.Errorf("expected increase in lastPingTime from %v, but got %v", oldPingTime, newPingTime) - } -} \ No newline at end of file + assert.GreaterOrEqual(tester, newPingTime.Sub(oldPingTime).Milliseconds(), int64(3)) +} From e08914c2a43866db6d6cea037b61bc9a765e9214 Mon Sep 17 00:00:00 2001 From: William Wernert Date: Fri, 3 Sep 2021 12:39:04 -0400 Subject: [PATCH 10/32] Refactor `config.clientparamaters.Verify()` The err value was being overwritten on each line of this function, so check the value after each `Verify()` call and return if err is not nil (i.e. the Verify() call failed) --- config/clientparameters.go | 170 +++++++++++++++++++------------------ 1 file changed, 86 insertions(+), 84 deletions(-) diff --git a/config/clientparameters.go b/config/clientparameters.go index 779f17de..0fcc73c1 100644 --- a/config/clientparameters.go +++ b/config/clientparameters.go @@ -17,115 +17,117 @@ const DEFAULT_RELATIVE_TIME_UNIT = 30 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"` - ReleaseNotesUrl string `json:"releaseNotesUrl"` - GridParams GridParameters `json:"grid"` - WebSocketTimeoutMs int `json:"webSocketTimeoutMs"` - TipTimeoutMs int `json:"tipTimeoutMs"` - ApiTimeoutMs int `json:"apiTimeoutMs"` - CacheExpirationMs int `json:"cacheExpirationMs"` - InactiveTools []string `json:"inactiveTools"` - Tools []ClientTool `json:"tools"` + HuntingParams HuntingParameters `json:"hunt"` + AlertingParams HuntingParameters `json:"alerts"` + JobParams HuntingParameters `json:"job"` + DocsUrl string `json:"docsUrl"` + CheatsheetUrl string `json:"cheatsheetUrl"` + ReleaseNotesUrl string `json:"releaseNotesUrl"` + GridParams GridParameters `json:"grid"` + WebSocketTimeoutMs int `json:"webSocketTimeoutMs"` + TipTimeoutMs int `json:"tipTimeoutMs"` + ApiTimeoutMs int `json:"apiTimeoutMs"` + CacheExpirationMs int `json:"cacheExpirationMs"` + InactiveTools []string `json:"inactiveTools"` + Tools []ClientTool `json:"tools"` } func (config *ClientParameters) Verify() error { - var err error - err = config.HuntingParams.Verify() - err = config.AlertingParams.Verify() - err = config.JobParams.Verify() - return err + if err := config.HuntingParams.Verify(); err != nil { + return err + } + if err := config.AlertingParams.Verify(); err != nil { + return err + } + return config.JobParams.Verify() } type ClientTool struct { - Name string `json:"name"` - Description string `json:"description"` - Target string `json:"target"` - Icon string `json:"icon"` - Link string `json:"link"` + Name string `json:"name"` + Description string `json:"description"` + Target string `json:"target"` + Icon string `json:"icon"` + Link string `json:"link"` } type HuntingQuery struct { - Name string `json:"name"` - Description string `json:"description"` - Query string `json:"query"` + Name string `json:"name"` + Description string `json:"description"` + Query string `json:"query"` } type HuntingAction struct { - Name string `json:"name"` - Description string `json:"description"` - Icon string `json:"icon"` - Link string `json:"link"` - Links []string `json:"links"` - Fields []string `json:"fields"` - Target string `json:"target"` - Background bool `json:"background"` - BackgroundSuccessLink string `json:"backgroundSuccessLink"` - BackgroundFailureLink string `json:"backgroundFailureLink"` - Method string `json:"method"` - Body string `json:"body"` - Options map[string]interface{} `json:"options"` + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon"` + Link string `json:"link"` + Links []string `json:"links"` + Fields []string `json:"fields"` + Target string `json:"target"` + Background bool `json:"background"` + BackgroundSuccessLink string `json:"backgroundSuccessLink"` + BackgroundFailureLink string `json:"backgroundFailureLink"` + Method string `json:"method"` + Body string `json:"body"` + Options map[string]interface{} `json:"options"` } type ToggleFilter struct { - Name string `json:"name"` - Filter string `json:"filter"` - Enabled bool `json:"enabled"` - Exclusive bool `json:"exclusive"` - EnablesToggles []string `json:"enablesToggles"` - DisablesToggles []string `json:"disablesToggles"` + Name string `json:"name"` + Filter string `json:"filter"` + Enabled bool `json:"enabled"` + Exclusive bool `json:"exclusive"` + EnablesToggles []string `json:"enablesToggles"` + DisablesToggles []string `json:"disablesToggles"` } type HuntingParameters struct { - GroupItemsPerPage int `json:"groupItemsPerPage"` - GroupFetchLimit int `json:"groupFetchLimit"` - EventItemsPerPage int `json:"eventItemsPerPage"` - EventFetchLimit int `json:"eventFetchLimit"` - RelativeTimeValue int `json:"relativeTimeValue"` - RelativeTimeUnit int `json:"relativeTimeUnit"` - MostRecentlyUsedLimit int `json:"mostRecentlyUsedLimit"` - EventFields map[string][]string `json:"eventFields"` - QueryBaseFilter string `json:"queryBaseFilter"` - QueryToggleFilters []*ToggleFilter `json:"queryToggleFilters"` - Queries []*HuntingQuery `json:"queries"` - Actions []*HuntingAction `json:"actions"` - Advanced bool `json:"advanced"` - AckEnabled bool `json:"ackEnabled"` - EscalateEnabled bool `json:"escalateEnabled"` + GroupItemsPerPage int `json:"groupItemsPerPage"` + GroupFetchLimit int `json:"groupFetchLimit"` + EventItemsPerPage int `json:"eventItemsPerPage"` + EventFetchLimit int `json:"eventFetchLimit"` + RelativeTimeValue int `json:"relativeTimeValue"` + RelativeTimeUnit int `json:"relativeTimeUnit"` + MostRecentlyUsedLimit int `json:"mostRecentlyUsedLimit"` + EventFields map[string][]string `json:"eventFields"` + QueryBaseFilter string `json:"queryBaseFilter"` + QueryToggleFilters []*ToggleFilter `json:"queryToggleFilters"` + Queries []*HuntingQuery `json:"queries"` + Actions []*HuntingAction `json:"actions"` + Advanced bool `json:"advanced"` + AckEnabled bool `json:"ackEnabled"` + EscalateEnabled bool `json:"escalateEnabled"` } type GridParameters struct { } func (params *HuntingParameters) Verify() error { - var err error - if params.GroupFetchLimit <= 0 { - params.GroupFetchLimit = DEFAULT_GROUP_FETCH_LIMIT - } - if params.EventFetchLimit <= 0 { - params.EventFetchLimit = DEFAULT_EVENT_FETCH_LIMIT - } - if params.RelativeTimeValue <= 0 { - params.RelativeTimeValue = DEFAULT_RELATIVE_TIME_VALUE - } - if params.RelativeTimeUnit <= 0 { - params.RelativeTimeUnit = DEFAULT_RELATIVE_TIME_UNIT - } - if params.MostRecentlyUsedLimit < 10 { - params.MostRecentlyUsedLimit = DEFAULT_MOST_RECENTLY_USED_LIMIT - } - params.combineDeprecatedLinkIntoLinks() - return err + var err error + if params.GroupFetchLimit <= 0 { + params.GroupFetchLimit = DEFAULT_GROUP_FETCH_LIMIT + } + if params.EventFetchLimit <= 0 { + params.EventFetchLimit = DEFAULT_EVENT_FETCH_LIMIT + } + if params.RelativeTimeValue <= 0 { + params.RelativeTimeValue = DEFAULT_RELATIVE_TIME_VALUE + } + if params.RelativeTimeUnit <= 0 { + params.RelativeTimeUnit = DEFAULT_RELATIVE_TIME_UNIT + } + if params.MostRecentlyUsedLimit < 10 { + params.MostRecentlyUsedLimit = DEFAULT_MOST_RECENTLY_USED_LIMIT + } + params.combineDeprecatedLinkIntoLinks() + return err } func (params *HuntingParameters) combineDeprecatedLinkIntoLinks() { - for _, action := range params.Actions { - if len(action.Link) > 0 { - action.Links = append(action.Links, action.Link) - action.Link = "" - } - } + for _, action := range params.Actions { + if len(action.Link) > 0 { + action.Links = append(action.Links, action.Link) + action.Link = "" + } + } } From b074148ef621ed6ba4437bab3c617c64f20f9ee6 Mon Sep 17 00:00:00 2001 From: William Wernert Date: Fri, 10 Sep 2021 15:16:05 -0400 Subject: [PATCH 11/32] Add node images for cloud instances --- html/images/appliances/so-cloud-aws.jpg | Bin 0 -> 59231 bytes html/images/appliances/so-cloud-azure.jpg | Bin 0 -> 54913 bytes html/images/appliances/so-cloud-gcp.jpg | Bin 0 -> 58437 bytes model/node.go | 145 +++++++++++----------- 4 files changed, 74 insertions(+), 71 deletions(-) create mode 100644 html/images/appliances/so-cloud-aws.jpg create mode 100644 html/images/appliances/so-cloud-azure.jpg create mode 100644 html/images/appliances/so-cloud-gcp.jpg diff --git a/html/images/appliances/so-cloud-aws.jpg b/html/images/appliances/so-cloud-aws.jpg new file mode 100644 index 0000000000000000000000000000000000000000..004f50594580366b6da6ac9768bc8aaada61645c GIT binary patch literal 59231 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

-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 F{tI&s>W}~c literal 0 HcmV?d00001 diff --git a/model/node.go b/model/node.go index 2f06ab1b..9a210ff7 100644 --- a/model/node.go +++ b/model/node.go @@ -11,94 +11,97 @@ package model import ( - "time" + "time" ) -const NodeStatusUnknown = "unknown" -const NodeStatusOk = "ok" -const NodeStatusFault = "fault" +const NodeStatusUnknown = "unknown" +const NodeStatusOk = "ok" +const NodeStatusFault = "fault" type Node struct { - Id string `json:"id"` - OnlineTime time.Time `json:"onlineTime"` - UpdateTime time.Time `json:"updateTime"` - EpochTime time.Time `json:"epochTime"` - UptimeSeconds int `json:"uptimeSeconds"` - Description string `json:"description"` - Address string `json:"address"` - Role string `json:"role"` - Model string `json:"model"` - ImageFront string `json:"imageFront"` - ImageBack string `json:"imageBack"` - Status string `json:"status"` - Version string `json:"version"` - ConnectionStatus string `json:"connectionStatus"` - RaidStatus string `json:"raidStatus"` - ProcessStatus string `json:"processStatus"` - ProductionEps int `json:"productionEps"` - ConsumptionEps int `json:"consumptionEps"` - FailedEvents int `json:"failedEvents"` - MetricsEnabled bool `json:"metricsEnabled"` + Id string `json:"id"` + OnlineTime time.Time `json:"onlineTime"` + UpdateTime time.Time `json:"updateTime"` + EpochTime time.Time `json:"epochTime"` + UptimeSeconds int `json:"uptimeSeconds"` + Description string `json:"description"` + Address string `json:"address"` + Role string `json:"role"` + Model string `json:"model"` + ImageFront string `json:"imageFront"` + ImageBack string `json:"imageBack"` + Status string `json:"status"` + Version string `json:"version"` + ConnectionStatus string `json:"connectionStatus"` + RaidStatus string `json:"raidStatus"` + ProcessStatus string `json:"processStatus"` + ProductionEps int `json:"productionEps"` + ConsumptionEps int `json:"consumptionEps"` + FailedEvents int `json:"failedEvents"` + MetricsEnabled bool `json:"metricsEnabled"` } func NewNode(id string) *Node { - return &Node{ - Id: id, - Status: NodeStatusUnknown, - ConnectionStatus: NodeStatusUnknown, - RaidStatus: NodeStatusUnknown, - ProcessStatus: NodeStatusUnknown, - OnlineTime: time.Now(), - UpdateTime: time.Now(), - } + return &Node{ + Id: id, + Status: NodeStatusUnknown, + ConnectionStatus: NodeStatusUnknown, + RaidStatus: NodeStatusUnknown, + ProcessStatus: NodeStatusUnknown, + OnlineTime: time.Now(), + UpdateTime: time.Now(), + } } func (node *Node) SetModel(model string) { - node.Model = model - switch model { - case "SOSMN", "SOS500", "SOS1000": - node.ImageFront = "sos-1u-front-thumb.jpg" - node.ImageBack = "sos-1u-ethernet-back-thumb.jpg" - case "SOS1000F", "SOS10K", "SOSSNNV": - node.ImageFront = "sos-1u-front-thumb.jpg" - node.ImageBack = "sos-1u-sfp-back-thumb.jpg" - case "SOS4000", "SOSSN7200": - node.ImageFront = "sos-2u-front-thumb.jpg" - node.ImageBack = "sos-2u-back-thumb.jpg" - case "SO2AMI01": - case "SO2AZI01": - case "SO2GCI01": - default: - node.Model = "N/A" - } + node.Model = model + switch model { + case "SOSMN", "SOS500", "SOS1000": + node.ImageFront = "sos-1u-front-thumb.jpg" + node.ImageBack = "sos-1u-ethernet-back-thumb.jpg" + case "SOS1000F", "SOS10K", "SOSSNNV": + node.ImageFront = "sos-1u-front-thumb.jpg" + node.ImageBack = "sos-1u-sfp-back-thumb.jpg" + case "SOS4000", "SOSSN7200": + node.ImageFront = "sos-2u-front-thumb.jpg" + node.ImageBack = "sos-2u-back-thumb.jpg" + case "SO2AMI01": + node.ImageFront = "so-cloud-aws.jpg" + case "SO2AZI01": + node.ImageFront = "so-cloud-azure.jpg" + case "SO2GCI01": + node.ImageFront = "so-cloud-gcp.jpg" + default: + node.Model = "N/A" + } } func (node *Node) updateStatusComponent(currentState string, newState string) string { - if newState != NodeStatusUnknown { - if currentState == NodeStatusOk || currentState == NodeStatusUnknown { - currentState = newState - } - } + if newState != NodeStatusUnknown { + if currentState == NodeStatusOk || currentState == NodeStatusUnknown { + currentState = newState + } + } - return currentState + return currentState } func (node *Node) UpdateOverallStatus(enhancedStatusEnabled bool) bool { - newStatus := NodeStatusUnknown - newStatus = node.updateStatusComponent(newStatus, node.ConnectionStatus) - if enhancedStatusEnabled { - newStatus = node.updateStatusComponent(newStatus, node.RaidStatus) - newStatus = node.updateStatusComponent(newStatus, node.ProcessStatus) - } + newStatus := NodeStatusUnknown + newStatus = node.updateStatusComponent(newStatus, node.ConnectionStatus) + 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 (enhancedStatusEnabled && node.ProcessStatus == NodeStatusUnknown) || - node.ConnectionStatus == NodeStatusUnknown { - newStatus = NodeStatusFault - } + // Special case: If either process or connection status is unknown then show node in error state. + if (enhancedStatusEnabled && node.ProcessStatus == NodeStatusUnknown) || + node.ConnectionStatus == NodeStatusUnknown { + newStatus = NodeStatusFault + } - oldStatus := node.Status - node.Status = newStatus - node.MetricsEnabled = enhancedStatusEnabled - return oldStatus != node.Status + oldStatus := node.Status + node.Status = newStatus + node.MetricsEnabled = enhancedStatusEnabled + return oldStatus != node.Status } From e074b22ab9a299431a8360fd1d80667f2952cdda Mon Sep 17 00:00:00 2001 From: William Wernert Date: Fri, 10 Sep 2021 15:43:43 -0400 Subject: [PATCH 12/32] Fix unit test for cloud images --- model/node_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/model/node_test.go b/model/node_test.go index 01de16f3..d4aac421 100644 --- a/model/node_test.go +++ b/model/node_test.go @@ -35,9 +35,9 @@ func TestSetModel(tester *testing.T) { testModel(tester, "SOS10K", "SOS10K", "sos-1u-front-thumb.jpg", "sos-1u-sfp-back-thumb.jpg") testModel(tester, "SOS4000", "SOS4000", "sos-2u-front-thumb.jpg", "sos-2u-back-thumb.jpg") testModel(tester, "SOSSN7200", "SOSSN7200", "sos-2u-front-thumb.jpg", "sos-2u-back-thumb.jpg") - testModel(tester, "SO2AMI01", "SO2AMI01", "", "") - testModel(tester, "SO2AZI01", "SO2AZI01", "", "") - testModel(tester, "SO2GCI01", "SO2GCI01", "", "") + testModel(tester, "SO2AMI01", "SO2AMI01", "so-cloud-aws.jpg", "") + testModel(tester, "SO2AZI01", "SO2AZI01", "so-cloud-azure.jpg", "") + testModel(tester, "SO2GCI01", "SO2GCI01", "so-cloud-gcp.jpg", "") } func testStatus(tester *testing.T, From 25cc851ab83ed3d3f1e71509f99786e17fb85157 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Tue, 14 Sep 2021 19:46:58 -0400 Subject: [PATCH 13/32] Continuation of auth enhancements --- model/job.go | 44 +-- model/job_test.go | 17 +- rbac/permissions | 22 +- rbac/roles | 12 +- server/datastore.go | 25 +- server/jobhandler.go | 49 +-- server/jobshandler.go | 13 +- server/modules/elastic/joblookuphandler.go | 19 +- server/modules/filedatastore/filedatastore.go | 10 +- .../filedatastore/filedatastoreimpl.go | 310 +++++++++++------- .../filedatastore/filedatastoreimpl_test.go | 104 +++++- .../statickeyauth/statickeyauthimpl.go | 14 +- .../statickeyauth/statickeyauthimpl_test.go | 7 +- .../staticrbac/staticrbacauthorizer.go | 21 +- server/nodehandler.go | 13 +- server/packethandler.go | 11 +- server/streamhandler.go | 26 +- server/userstore.go | 2 +- 18 files changed, 456 insertions(+), 263 deletions(-) diff --git a/model/job.go b/model/job.go index 6023a61f..c3768754 100644 --- a/model/job.go +++ b/model/job.go @@ -21,29 +21,29 @@ const JobStatusIncomplete = 2 const JobStatusDeleted = 3 type Job struct { - Id int `json:"id"` - CreateTime time.Time `json:"createTime"` - Status int `json:"status"` - CompleteTime time.Time `json:"completeTime"` - FailTime time.Time `json:"failTime"` - Failure string `json:"failure"` - FailCount int `json:"failCount"` - Owner string `json:"owner"` - NodeId string `json:"nodeId"` - LegacySensorId string `json:"sensorId"` - FileExtension string `json:"fileExtension"` - Filter *Filter `json:"filter"` - UserId string `json:"userId"` + Id int `json:"id"` + CreateTime time.Time `json:"createTime"` + Status int `json:"status"` + CompleteTime time.Time `json:"completeTime"` + FailTime time.Time `json:"failTime"` + Failure string `json:"failure"` + FailCount int `json:"failCount"` + Owner string `json:"owner"` + NodeId string `json:"nodeId"` + LegacySensorId string `json:"sensorId"` + FileExtension string `json:"fileExtension"` + Filter *Filter `json:"filter"` + UserId string `json:"userId"` } func NewJob() *Job { return &Job{ - CreateTime: time.Now(), - Status: JobStatusPending, - Failure: "", - FailCount: 0, + CreateTime: time.Now(), + Status: JobStatusPending, + Failure: "", + FailCount: 0, FileExtension: "bin", - Filter: NewFilter(), + Filter: NewFilter(), } } @@ -52,7 +52,7 @@ func (job *Job) SetNodeId(nodeId string) { } func (job *Job) GetNodeId() string { - // Lower case on the Getter as well since the property could have been + // Lower case on the Getter as well since the property could have been // manipulated directly. Consider json.Unmarshall(). job.NodeId = strings.ToLower(job.NodeId) if len(job.NodeId) == 0 { @@ -63,6 +63,10 @@ func (job *Job) GetNodeId() string { return job.NodeId } +func (job *Job) CanProcess() bool { + return job.Status != JobStatusCompleted && job.Status != JobStatusDeleted +} + func (job *Job) Complete() { job.Status = JobStatusCompleted job.CompleteTime = time.Now() @@ -73,4 +77,4 @@ func (job *Job) Fail(err error) { job.Failure = err.Error() job.FailTime = time.Now() job.FailCount++ -} \ No newline at end of file +} diff --git a/model/job_test.go b/model/job_test.go index cd941018..5fb3e95f 100644 --- a/model/job_test.go +++ b/model/job_test.go @@ -12,9 +12,8 @@ package model import ( "errors" - "testing" - "github.com/stretchr/testify/assert" + "testing" ) func TestVerifyJob(tester *testing.T) { @@ -66,3 +65,17 @@ func TestGetLegacyNodeId(tester *testing.T) { job.NodeId = "" assert.Equal(tester, "bar", job.GetNodeId()) } + +func TestCanProcess(tester *testing.T) { + job := NewJob() + assert.True(tester, job.CanProcess()) + job.Fail(errors.New("Something")) + assert.True(tester, job.CanProcess()) + + job.Complete() + assert.False(tester, job.CanProcess()) + + job = NewJob() + job.Status = JobStatusDeleted + assert.False(tester, job.CanProcess()) +} diff --git a/rbac/permissions b/rbac/permissions index e2f2c326..c7f8b906 100644 --- a/rbac/permissions +++ b/rbac/permissions @@ -1,16 +1,22 @@ # Define low-level permissions and which permission set roles can use them -# Syntax => permX: roleY roleZ +# Syntax => permX: roleY roleZ # Explanation => roleY and roleZ are granted permission permX -grid/read: grid-monitor -roles/read: user-monitor -roles/write: user-admin -users/read: user-monitor -users/write: user-admin +grid/read: grid-monitor +roles/read: user-monitor +roles/write: user-admin +users/read: user-monitor +users/write: user-admin +jobs/read: job-monitor +jobs/write: job-admin +jobs/delete: job-admin +jobs/process: job-processor # Define low-level permission set inheritence relationships -# Syntax => roleB: roleA +# Syntax => roleB: roleA # Explanation => roleA inherits all of roleA's permissions -user-monitor: user-admin \ No newline at end of file +user-monitor: user-admin +job-monitor: job-admin +grid-monitor: grid-admin \ No newline at end of file diff --git a/rbac/roles b/rbac/roles index 50879843..a4af28ad 100644 --- a/rbac/roles +++ b/rbac/roles @@ -1,7 +1,11 @@ # Define which business level roles can access which permission set roles. -# Syntax => roleX: roleY roleZ +# Syntax => roleX: roleY roleZ # Explanation => roleY and roleZ are granted permissions of roleX -grid-monitor: superuser analyst -user-monitor: analyst -user-admin: superuser \ No newline at end of file +grid-admin: superuser +grid-monitor: analyst auditor +user-admin: superuser +user-monitor: analyst auditor +job-admin: superuser analyst +job-monitor: auditor +job-processor: sensor diff --git a/server/datastore.go b/server/datastore.go index 6d2d0c55..d55c0b34 100644 --- a/server/datastore.go +++ b/server/datastore.go @@ -11,8 +11,9 @@ package server import ( - "io" + "context" "github.com/security-onion-solutions/securityonion-soc/model" + "io" ) type Datastore interface { @@ -20,14 +21,14 @@ type Datastore interface { GetNodes() []*model.Node AddNode(node *model.Node) error UpdateNode(newNode *model.Node) (*model.Node, error) - GetNextJob(nodeId string) *model.Job - CreateJob() *model.Job - GetJob(jobId int) *model.Job - GetJobs() []*model.Job - AddJob(job *model.Job) error - UpdateJob(job *model.Job) error - DeleteJob(job *model.Job) error - GetPackets(jobId int, offset int, count int, unwrap bool) ([]*model.Packet, error) - SavePacketStream(jobId int, reader io.ReadCloser) error - GetPacketStream(jobId int, unwrap bool) (io.ReadCloser, string, int64, error) -} \ No newline at end of file + GetNextJob(ctx context.Context, nodeId string) *model.Job + CreateJob(ctx context.Context) *model.Job + GetJob(ctx context.Context, jobId int) *model.Job + GetJobs(ctx context.Context) []*model.Job + AddJob(ctx context.Context, job *model.Job) error + UpdateJob(ctx context.Context, job *model.Job) error + DeleteJob(ctx context.Context, job *model.Job) error + GetPackets(ctx context.Context, jobId int, offset int, count int, unwrap bool) ([]*model.Packet, error) + SavePacketStream(ctx context.Context, jobId int, reader io.ReadCloser) error + GetPacketStream(ctx context.Context, jobId int, unwrap bool) (io.ReadCloser, string, int64, error) +} diff --git a/server/jobhandler.go b/server/jobhandler.go index a981aa91..d6b1d166 100644 --- a/server/jobhandler.go +++ b/server/jobhandler.go @@ -13,20 +13,20 @@ package server import ( "context" "errors" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/web" "net/http" "regexp" "strconv" - "github.com/security-onion-solutions/securityonion-soc/model" - "github.com/security-onion-solutions/securityonion-soc/web" ) type JobHandler struct { web.BaseHandler - server *Server + server *Server } func NewJobHandler(srv *Server) *JobHandler { - handler := &JobHandler {} + handler := &JobHandler{} handler.Host = srv.Host handler.server = srv handler.Impl = handler @@ -35,10 +35,14 @@ func NewJobHandler(srv *Server) *JobHandler { 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(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) + 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") } @@ -46,7 +50,7 @@ func (jobHandler *JobHandler) HandleNow(ctx context.Context, writer http.Respons 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)) + job := jobHandler.server.Datastore.GetJob(ctx, int(jobId)) if job != nil { statusCode = http.StatusOK } else { @@ -57,12 +61,12 @@ func (jobHandler *JobHandler) get(ctx context.Context, writer http.ResponseWrite func (jobHandler *JobHandler) post(ctx context.Context, writer http.ResponseWriter, request *http.Request) (int, interface{}, error) { statusCode := http.StatusBadRequest - job := jobHandler.server.Datastore.CreateJob() + job := jobHandler.server.Datastore.CreateJob(ctx) err := jobHandler.ReadJson(request, job) if err == nil { if user, ok := ctx.Value(web.ContextKeyRequestor).(*model.User); ok { job.UserId = user.Id - err = jobHandler.server.Datastore.AddJob(job) + err = jobHandler.server.Datastore.AddJob(ctx, job) if err == nil { jobHandler.Host.Broadcast("job", job) statusCode = http.StatusCreated @@ -79,10 +83,17 @@ func (jobHandler *JobHandler) put(ctx context.Context, writer http.ResponseWrite job := model.NewJob() err := jobHandler.ReadJson(request, job) if err == nil { - err = jobHandler.server.Datastore.UpdateJob(job) - if err == nil { - jobHandler.Host.Broadcast("job", job) - statusCode = http.StatusOK + existingJob := jobHandler.server.Datastore.GetJob(ctx, job.Id) + if existingJob != nil { + job.UserId = existingJob.UserId // Prevent users from altering the creating user + + err = jobHandler.server.Datastore.UpdateJob(ctx, job) + if err == nil { + jobHandler.Host.Broadcast("job", job) + statusCode = http.StatusOK + } else { + statusCode = http.StatusNotFound + } } else { statusCode = http.StatusNotFound } @@ -97,14 +108,14 @@ func (jobHandler *JobHandler) delete(ctx context.Context, writer http.ResponseWr safe, _ := regexp.MatchString(`^[0-9-]+$`, id) if !safe { return http.StatusBadRequest, nil, errors.New("Invalid id") - } + } statusCode := http.StatusBadRequest jobId, err := strconv.Atoi(id) if err == nil { - job := jobHandler.server.Datastore.GetJob(int(jobId)) + job := jobHandler.server.Datastore.GetJob(ctx, int(jobId)) if job != nil { - err = jobHandler.server.Datastore.DeleteJob(job) + err = jobHandler.server.Datastore.DeleteJob(ctx, job) if err == nil { jobHandler.Host.Broadcast("job", job) statusCode = http.StatusOK @@ -115,4 +126,4 @@ func (jobHandler *JobHandler) delete(ctx context.Context, writer http.ResponseWr } return statusCode, nil, err -} \ No newline at end of file +} diff --git a/server/jobshandler.go b/server/jobshandler.go index b779b04e..934ac763 100644 --- a/server/jobshandler.go +++ b/server/jobshandler.go @@ -13,17 +13,17 @@ package server import ( "context" "errors" - "net/http" "github.com/security-onion-solutions/securityonion-soc/web" + "net/http" ) type JobsHandler struct { web.BaseHandler - server *Server + server *Server } func NewJobsHandler(srv *Server) *JobsHandler { - handler := &JobsHandler {} + handler := &JobsHandler{} handler.Host = srv.Host handler.server = srv handler.Impl = handler @@ -32,11 +32,12 @@ func NewJobsHandler(srv *Server) *JobsHandler { 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(ctx, writer, request) + case http.MethodGet: + return jobsHandler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } 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 + return http.StatusOK, jobsHandler.server.Datastore.GetJobs(ctx), nil +} diff --git a/server/modules/elastic/joblookuphandler.go b/server/modules/elastic/joblookuphandler.go index aa4558e5..9d8f8f8e 100644 --- a/server/modules/elastic/joblookuphandler.go +++ b/server/modules/elastic/joblookuphandler.go @@ -13,21 +13,21 @@ package elastic import ( "context" "errors" - "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" + "net/http" + "strconv" ) type JobLookupHandler struct { web.BaseHandler - server *server.Server - store *ElasticEventstore + server *server.Server + store *ElasticEventstore } func NewJobLookupHandler(srv *server.Server, store *ElasticEventstore) *JobLookupHandler { - handler := &JobLookupHandler {} + handler := &JobLookupHandler{} handler.Host = srv.Host handler.server = srv handler.BaseHandler.Impl = handler @@ -37,7 +37,8 @@ func NewJobLookupHandler(srv *server.Server, store *ElasticEventstore) *JobLooku 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(ctx, writer, request) + case http.MethodGet: + return handler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } @@ -46,7 +47,7 @@ func (handler *JobLookupHandler) get(ctx context.Context, writer http.ResponseWr statusCode := http.StatusBadRequest timestampStr := request.URL.Query().Get("time") // Elastic doc timestamp - + idField := "_id" idValue := request.URL.Query().Get("esid") // Elastic doc ID if len(idValue) == 0 { @@ -54,12 +55,12 @@ func (handler *JobLookupHandler) get(ctx context.Context, writer http.ResponseWr idField = "network.community_id" } - job := handler.server.Datastore.CreateJob() + job := handler.server.Datastore.CreateJob(ctx) 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 - err = handler.server.Datastore.AddJob(job) + err = handler.server.Datastore.AddJob(ctx, job) if err == nil { handler.Host.Broadcast("job", job) statusCode = http.StatusOK diff --git a/server/modules/filedatastore/filedatastore.go b/server/modules/filedatastore/filedatastore.go index 39954535..8a996755 100644 --- a/server/modules/filedatastore/filedatastore.go +++ b/server/modules/filedatastore/filedatastore.go @@ -16,15 +16,15 @@ import ( ) type FileDatastore struct { - config module.ModuleConfig - server *server.Server - impl *FileDatastoreImpl + config module.ModuleConfig + server *server.Server + impl *FileDatastoreImpl } func NewFileDatastore(srv *server.Server) *FileDatastore { - return &FileDatastore { + return &FileDatastore{ server: srv, - impl: NewFileDatastoreImpl(), + impl: NewFileDatastoreImpl(srv), } } diff --git a/server/modules/filedatastore/filedatastoreimpl.go b/server/modules/filedatastore/filedatastoreimpl.go index f9a42119..7595b2d0 100644 --- a/server/modules/filedatastore/filedatastoreimpl.go +++ b/server/modules/filedatastore/filedatastoreimpl.go @@ -11,41 +11,46 @@ package filedatastore import ( + "context" "errors" "fmt" + "github.com/apex/log" + "github.com/kennygrant/sanitize" + "github.com/security-onion-solutions/securityonion-soc/json" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/module" + "github.com/security-onion-solutions/securityonion-soc/packet" + "github.com/security-onion-solutions/securityonion-soc/server" + "github.com/security-onion-solutions/securityonion-soc/web" "io" "os" "path/filepath" "strings" "sync" "time" - "github.com/apex/log" - "github.com/security-onion-solutions/securityonion-soc/json" - "github.com/security-onion-solutions/securityonion-soc/model" - "github.com/security-onion-solutions/securityonion-soc/module" - "github.com/security-onion-solutions/securityonion-soc/packet" - "github.com/kennygrant/sanitize" ) const DEFAULT_RETRY_FAILURE_INTERVAL_MS = 600000 type FileDatastoreImpl struct { - jobDir string - retryFailureIntervalMs int - jobsByNodeId map[string][]*model.Job - jobsById map[int]*model.Job - nodesById map[string]*model.Node - ready bool - nextJobId int - lock sync.RWMutex + server *server.Server + jobDir string + retryFailureIntervalMs int + jobsByNodeId map[string][]*model.Job + jobsById map[int]*model.Job + nodesById map[string]*model.Node + ready bool + nextJobId int + lock sync.RWMutex } -func NewFileDatastoreImpl() *FileDatastoreImpl { - return &FileDatastoreImpl { +func NewFileDatastoreImpl(srv *server.Server) *FileDatastoreImpl { + return &FileDatastoreImpl{ + server: srv, jobsByNodeId: make(map[string][]*model.Job), - jobsById: make(map[int]*model.Job), - nodesById: make(map[string]*model.Node), - lock: sync.RWMutex{}, + jobsById: make(map[int]*model.Job), + nodesById: make(map[string]*model.Node), + lock: sync.RWMutex{}, } } @@ -82,8 +87,8 @@ func (datastore *FileDatastoreImpl) AddNode(node *model.Node) error { func (datastore *FileDatastoreImpl) addNode(node *model.Node) *model.Node { datastore.nodesById[node.Id] = node - log.WithFields(log.Fields { - "id": node.Id, + log.WithFields(log.Fields{ + "id": node.Id, "description": node.Description, }).Debug("Added node") return node @@ -101,15 +106,15 @@ func (datastore *FileDatastoreImpl) UpdateNode(newNode *model.Node) (*model.Node } // Only copy the following values from the incoming node. Preserve everything else. - node.EpochTime = newNode.EpochTime - node.Role = newNode.Role - node.Description = newNode.Description - node.Address = newNode.Address - node.Version = newNode.Version + node.EpochTime = newNode.EpochTime + node.Role = newNode.Role + node.Description = newNode.Description + node.Address = newNode.Address + node.Version = newNode.Version // Ensure model parameters are updated node.SetModel(newNode.Model) - + // Mark ConnectionStatus as Ok since this node just checked in node.ConnectionStatus = model.NodeStatusOk @@ -122,73 +127,115 @@ func (datastore *FileDatastoreImpl) UpdateNode(newNode *model.Node) (*model.Node log.WithField("description", newNode.Description).Info("Not adding node with missing id") } return node, nil -} +} -func (datastore *FileDatastoreImpl) GetNextJob(nodeId string) *model.Job { +func (datastore *FileDatastoreImpl) GetNextJob(ctx context.Context, nodeId string) *model.Job { datastore.lock.RLock() defer datastore.lock.RUnlock() var nextJob *model.Job - now := time.Now() - jobs := datastore.jobsByNodeId[strings.ToLower(nodeId)] - for _, job := range jobs { - retryTime := job.FailTime.Add(time.Millisecond * time.Duration(datastore.retryFailureIntervalMs)) - if job.Status != model.JobStatusCompleted && - (nextJob == nil || job.CreateTime.Before(nextJob.CreateTime)) && - (job.Status != model.JobStatusIncomplete || retryTime.Before(now)) { - nextJob = job + + if err := datastore.server.Authorizer.CheckContextOperationAuthorized(ctx, "process", "jobs"); err == nil { + now := time.Now() + jobs := datastore.jobsByNodeId[strings.ToLower(nodeId)] + for _, job := range jobs { + retryTime := job.FailTime.Add(time.Millisecond * time.Duration(datastore.retryFailureIntervalMs)) + if job.Status != model.JobStatusCompleted && + (nextJob == nil || job.CreateTime.Before(nextJob.CreateTime)) && + (job.Status != model.JobStatusIncomplete || retryTime.Before(now)) { + nextJob = job + } } } return nextJob } -func (datastore *FileDatastoreImpl) CreateJob() *model.Job { +func (datastore *FileDatastoreImpl) CreateJob(ctx context.Context) *model.Job { datastore.lock.Lock() defer datastore.lock.Unlock() job := model.NewJob() job.Id = datastore.nextJobId datastore.incrementJobId(job.Id) - log.WithFields(log.Fields { - "id": job.Id, + log.WithFields(log.Fields{ + "id": job.Id, "nextJobId": datastore.nextJobId, }).Debug("Created job") return job } -func (datastore *FileDatastoreImpl) GetJob(jobId int) *model.Job { +func (datastore *FileDatastoreImpl) jobIsAllowed(ctx context.Context, job *model.Job, op string) bool { + allowed := false + + if job != nil { + if err := datastore.server.Authorizer.CheckContextOperationAuthorized(ctx, op, "jobs"); err == nil { + // User can operate on all jobs + allowed = true + } else { + // User is only authorized against their own jobs. + if user, ok := ctx.Value(web.ContextKeyRequestor).(*model.User); ok { + if job.UserId == user.Id { + allowed = true + } + } + } + } + return allowed +} + +func (datastore *FileDatastoreImpl) GetJob(ctx context.Context, jobId int) *model.Job { datastore.lock.RLock() defer datastore.lock.RUnlock() - return datastore.getJobById(jobId) + job := datastore.getJobById(jobId) + if job != nil { + if !datastore.jobIsAllowed(ctx, job, "read") { + // Do not return jobs that are not allowed to be viewed by this user. + job = nil + } + } + + return job } -func (datastore *FileDatastoreImpl) GetJobs() []*model.Job { +func (datastore *FileDatastoreImpl) GetJobs(ctx context.Context) []*model.Job { datastore.lock.RLock() defer datastore.lock.RUnlock() allJobs := make([]*model.Job, 0) for _, job := range datastore.jobsById { - allJobs = append(allJobs, job) + if datastore.jobIsAllowed(ctx, job, "read") { + allJobs = append(allJobs, job) + } } return allJobs } -func (datastore *FileDatastoreImpl) AddJob(job *model.Job) error { - datastore.lock.Lock() - defer datastore.lock.Unlock() - err := datastore.addJob(job) - if err == nil { - err = datastore.saveJob(job) +func (datastore *FileDatastoreImpl) AddJob(ctx context.Context, job *model.Job) error { + var err error + if err = datastore.server.Authorizer.CheckContextOperationAuthorized(ctx, "write", "jobs"); err == nil { + datastore.lock.Lock() + defer datastore.lock.Unlock() + err = datastore.addJob(job) + if err == nil { + err = datastore.saveJob(job) + } } return err } -func (datastore *FileDatastoreImpl) UpdateJob(job *model.Job) error { - datastore.lock.Lock() - defer datastore.lock.Unlock() - err := datastore.deleteJob(job) - if err == nil { - err = datastore.addJob(job) - if err == nil { - err = datastore.saveJob(job) +func (datastore *FileDatastoreImpl) UpdateJob(ctx context.Context, job *model.Job) error { + var err error + if err = datastore.server.Authorizer.CheckContextOperationAuthorized(ctx, "process", "jobs"); err == nil { + if job.CanProcess() { + datastore.lock.Lock() + defer datastore.lock.Unlock() + err = datastore.deleteJob(job) + if err == nil { + err = datastore.addJob(job) + if err == nil { + err = datastore.saveJob(job) + } + } + } else { + err = errors.New("Job is ineligible for processing") } } return err @@ -198,24 +245,27 @@ func (datastore *FileDatastoreImpl) getJobById(jobId int) *model.Job { return datastore.jobsById[jobId] } -func (datastore *FileDatastoreImpl) DeleteJob(job *model.Job) error { - err := datastore.deleteJob(job) - if err == nil { - job.Status = model.JobStatusDeleted - filename := fmt.Sprintf("%d.json", job.Id) - folder := filepath.Join(datastore.jobDir, sanitize.Name(job.GetNodeId())) - err = os.Remove(filepath.Join(folder, filename)) +func (datastore *FileDatastoreImpl) DeleteJob(ctx context.Context, job *model.Job) error { + var err error + if datastore.jobIsAllowed(ctx, job, "delete") { + err = datastore.deleteJob(job) if err == nil { - filename = fmt.Sprintf("%d.bin", job.Id) - os.Remove(filepath.Join(folder, filename)) - filename = fmt.Sprintf("%d.bin.unwrapped", job.Id) - os.Remove(filepath.Join(folder, filename)) - - log.WithFields(log.Fields { - "id": job.Id, - "folder": folder, - "filename": filename, - }).Info("Permanently deleted job and job files") + job.Status = model.JobStatusDeleted + filename := fmt.Sprintf("%d.json", job.Id) + folder := filepath.Join(datastore.jobDir, sanitize.Name(job.GetNodeId())) + err = os.Remove(filepath.Join(folder, filename)) + if err == nil { + filename = fmt.Sprintf("%d.bin", job.Id) + os.Remove(filepath.Join(folder, filename)) + filename = fmt.Sprintf("%d.bin.unwrapped", job.Id) + os.Remove(filepath.Join(folder, filename)) + + log.WithFields(log.Fields{ + "id": job.Id, + "folder": folder, + "filename": filename, + }).Info("Permanently deleted job and job files") + } } } return err @@ -236,8 +286,8 @@ func (datastore *FileDatastoreImpl) deleteJob(job *model.Job) error { } datastore.jobsByNodeId[job.GetNodeId()] = newJobs delete(datastore.jobsById, job.Id) - log.WithFields(log.Fields { - "id": job.Id, + log.WithFields(log.Fields{ + "id": job.Id, "node": job.GetNodeId(), }).Debug("Deleted job from list") } @@ -257,8 +307,8 @@ func (datastore *FileDatastoreImpl) addJob(job *model.Job) error { datastore.jobsByNodeId[job.GetNodeId()] = append(jobs, job) datastore.jobsById[job.Id] = job datastore.incrementJobId(job.Id) - log.WithFields(log.Fields { - "id": job.Id, + log.WithFields(log.Fields{ + "id": job.Id, "node": job.GetNodeId(), }).Debug("Added job") } @@ -274,9 +324,9 @@ func (datastore *FileDatastoreImpl) incrementJobId(id int) { func (datastore *FileDatastoreImpl) saveJob(job *model.Job) error { filename := fmt.Sprintf("%d.json", job.Id) folder := filepath.Join(datastore.jobDir, sanitize.Name(job.GetNodeId())) - log.WithFields(log.Fields { - "id": job.Id, - "folder": folder, + log.WithFields(log.Fields{ + "id": job.Id, + "folder": folder, "filename": filename, }).Debug("Saving job file") os.MkdirAll(folder, os.ModePerm) @@ -308,17 +358,21 @@ func (datastore *FileDatastoreImpl) loadJobs() error { return err } -func (datastore *FileDatastoreImpl) GetPackets(jobId int, offset int, count int, unwrap bool) ([]*model.Packet, error) { +func (datastore *FileDatastoreImpl) GetPackets(ctx context.Context, jobId int, offset int, count int, unwrap bool) ([]*model.Packet, error) { var packets []*model.Packet var err error - job := datastore.GetJob(jobId) + job := datastore.GetJob(ctx, jobId) if job != nil { - if job.Status == model.JobStatusCompleted { - packets, err = packet.ParsePcap(datastore.getStreamFilename(job), offset, count, unwrap) - if err != nil { - log.WithError(err).WithField("jobId", job.Id).Warn("Failed to parse captured packets") - err = nil + if datastore.jobIsAllowed(ctx, job, "read") { + if job.Status == model.JobStatusCompleted { + packets, err = packet.ParsePcap(datastore.getStreamFilename(job), offset, count, unwrap) + if err != nil { + log.WithError(err).WithField("jobId", job.Id).Warn("Failed to parse captured packets") + err = nil + } } + } else { + err = errors.New("Job is inaccessible") } } else { err = errors.New("Job not found") @@ -327,55 +381,65 @@ func (datastore *FileDatastoreImpl) GetPackets(jobId int, offset int, count int, return packets, err } -func (datastore *FileDatastoreImpl) SavePacketStream(jobId int, reader io.ReadCloser) error { +func (datastore *FileDatastoreImpl) SavePacketStream(ctx context.Context, jobId int, reader io.ReadCloser) error { var err error - job := datastore.GetJob(jobId) - if job != nil { - var count int64 - file, err := os.Create(datastore.getStreamFilename(job)) - if err == nil { - defer file.Close() - count, err = io.Copy(file, reader) - } - if err != nil { - log.WithError(err).WithField("jobId", jobId).Error("Failed to write packet stream to file") + if err = datastore.server.Authorizer.CheckContextOperationAuthorized(ctx, "process", "jobs"); err == nil { + job := datastore.GetJob(ctx, jobId) + if job != nil { + if job.CanProcess() { + var count int64 + file, err := os.Create(datastore.getStreamFilename(job)) + if err == nil { + defer file.Close() + count, err = io.Copy(file, reader) + } + if err != nil { + log.WithError(err).WithField("jobId", jobId).Error("Failed to write packet stream to file") + } else { + log.WithFields(log.Fields{ + "bytes": count, + "jobId": jobId, + }).Info("Saved packet stream to file") + } + } else { + err = errors.New("Job is ineligible for processing") + } } else { - log.WithFields(log.Fields { - "bytes": count, - "jobId": jobId, - }).Info("Saved packet stream to file") + err = errors.New("Job not found") } - } else { - err = errors.New("Job not found") } return err } -func (datastore *FileDatastoreImpl) GetPacketStream(jobId int, unwrap bool) (io.ReadCloser, string, int64, error) { +func (datastore *FileDatastoreImpl) GetPacketStream(ctx context.Context, jobId int, unwrap bool) (io.ReadCloser, string, int64, error) { var reader io.ReadCloser var filename string var length int64 var err error - job := datastore.GetJob(jobId) + job := datastore.GetJob(ctx, jobId) if job != nil { - if job.Status == model.JobStatusCompleted { - filename = fmt.Sprintf("sensoroni_%s_%d.%s", sanitize.Name(job.GetNodeId()), job.Id, sanitize.Name(job.FileExtension)); - var file *os.File - file, err = os.Open(datastore.getModifiedStreamFilename(job, unwrap)) - if err != nil { - log.WithError(err).WithField("jobId", job.Id).Error("Failed to open packet stream") - } else { - reader = file - info, err := file.Stat() - length = info.Size() - log.WithFields(log.Fields { - "size": length, - "name": info.Name(), - }).Info("Streaming file") + if datastore.jobIsAllowed(ctx, job, "read") { + if job.Status == model.JobStatusCompleted { + filename = fmt.Sprintf("sensoroni_%s_%d.%s", sanitize.Name(job.GetNodeId()), job.Id, sanitize.Name(job.FileExtension)) + var file *os.File + file, err = os.Open(datastore.getModifiedStreamFilename(job, unwrap)) if err != nil { - log.WithError(err).WithField("jobId", job.Id).Error("Failed to open file stats") + log.WithError(err).WithField("jobId", job.Id).Error("Failed to open packet stream") + } else { + reader = file + info, err := file.Stat() + length = info.Size() + log.WithFields(log.Fields{ + "size": length, + "name": info.Name(), + }).Info("Streaming file") + if err != nil { + log.WithError(err).WithField("jobId", job.Id).Error("Failed to open file stats") + } } } + } else { + err = errors.New("Job is inaccessible") } } else { err = errors.New("Job not found") diff --git a/server/modules/filedatastore/filedatastoreimpl_test.go b/server/modules/filedatastore/filedatastoreimpl_test.go index 6bcd7e6c..c19e745f 100644 --- a/server/modules/filedatastore/filedatastoreimpl_test.go +++ b/server/modules/filedatastore/filedatastoreimpl_test.go @@ -11,15 +11,28 @@ package filedatastore import ( + "context" "os" "testing" + "github.com/security-onion-solutions/securityonion-soc/fake" + "github.com/security-onion-solutions/securityonion-soc/model" "github.com/security-onion-solutions/securityonion-soc/module" + "github.com/security-onion-solutions/securityonion-soc/web" "github.com/stretchr/testify/assert" ) +const MY_USER_ID = "123" +const ANOTHER_USER_ID = "124" + +func newContext() context.Context { + user := model.NewUser() + user.Id = MY_USER_ID + return context.WithValue(context.Background(), web.ContextKeyRequestor, user) +} + func TestFileDatastoreInit(tester *testing.T) { - ds := NewFileDatastoreImpl() + ds := NewFileDatastoreImpl(fake.NewUnauthorizedServer()) cfg := make(module.ModuleConfig) err := ds.Init(cfg) assert.Error(tester, err) @@ -35,7 +48,7 @@ func TestFileDatastoreInit(tester *testing.T) { } func TestNodes(tester *testing.T) { - ds := NewFileDatastoreImpl() + ds := NewFileDatastoreImpl(fake.NewUnauthorizedServer()) cfg := make(module.ModuleConfig) ds.Init(cfg) node := ds.CreateNode("foo") @@ -55,22 +68,22 @@ func TestNodes(tester *testing.T) { ds.addNode(node) nodes = ds.GetNodes() assert.Len(tester, nodes, 2) - job := ds.GetNextJob("foo") + job := ds.GetNextJob(newContext(), "foo") assert.Nil(tester, job) } func TestJobs(tester *testing.T) { - ds := NewFileDatastoreImpl() + ds := NewFileDatastoreImpl(fake.NewAuthorizedServer(nil)) cfg := make(module.ModuleConfig) ds.Init(cfg) node := ds.CreateNode("foo") ds.addNode(node) // Test adding a job - job := ds.CreateJob() + job := ds.CreateJob(newContext()) assert.Equal(tester, 1001, job.Id) ds.addJob(job) - job = ds.CreateJob() + job = ds.CreateJob(newContext()) assert.Equal(tester, 1002, job.Id) ds.addJob(job) @@ -78,30 +91,95 @@ func TestJobs(tester *testing.T) { job = ds.getJobById(1001) assert.Equal(tester, 1001, job.Id) - job = ds.GetJob(1002) + job = ds.GetJob(newContext(), 1002) assert.Equal(tester, 1002, job.Id) - job = ds.GetJob(1003) + job = ds.GetJob(newContext(), 1003) assert.Nil(tester, job) // Test fetching all jobs - jobs := ds.GetJobs() + jobs := ds.GetJobs(newContext()) assert.Len(tester, jobs, 2) // Test deleting jobs ds.deleteJob(jobs[0]) - jobs = ds.GetJobs() + jobs = ds.GetJobs(newContext()) assert.Len(tester, jobs, 1) ds.deleteJob(jobs[0]) - jobs = ds.GetJobs() + jobs = ds.GetJobs(newContext()) assert.Len(tester, jobs, 0) } +func TestJobReadAuthorization(tester *testing.T) { + ds := NewFileDatastoreImpl(fake.NewUnauthorizedServer()) + cfg := make(module.ModuleConfig) + ds.Init(cfg) + node := ds.CreateNode("foo") + ds.addNode(node) + + myJobId := 10001 + anothersJobId := 10002 + + // Test adding a job + job := ds.CreateJob(newContext()) + job.UserId = ANOTHER_USER_ID + job.Id = anothersJobId + ds.addJob(job) + + job = ds.CreateJob(newContext()) + job.UserId = MY_USER_ID // This user's job + job.Id = myJobId + ds.addJob(job) + + job = ds.GetJob(newContext(), myJobId) + assert.Equal(tester, myJobId, job.Id) + + job = ds.GetJob(newContext(), anothersJobId) + assert.Nil(tester, job) + + // Test fetching all jobs + jobs := ds.GetJobs(newContext()) + assert.Len(tester, jobs, 1) // Only has my job +} + +func TestJobDeleteAuthorization(tester *testing.T) { + ds := NewFileDatastoreImpl(fake.NewUnauthorizedServer()) + cfg := make(module.ModuleConfig) + ds.Init(cfg) + node := ds.CreateNode("foo") + ds.addNode(node) + + myJobId := 10001 + anothersJobId := 10002 + + // Test adding a job + anotherJob := ds.CreateJob(newContext()) + anotherJob.UserId = ANOTHER_USER_ID + anotherJob.Id = anothersJobId + ds.addJob(anotherJob) + + myJob := ds.CreateJob(newContext()) + myJob.UserId = MY_USER_ID // This user's job + myJob.Id = myJobId + ds.addJob(myJob) + + assert.NotNil(tester, ds.jobsById[myJobId]) + assert.NotNil(tester, ds.jobsById[anothersJobId]) + + // Should not delete another user's job + ds.DeleteJob(newContext(), anotherJob) + assert.NotNil(tester, ds.jobsById[anothersJobId]) + + // Should delete my own job + ds.DeleteJob(newContext(), myJob) + assert.Nil(tester, ds.jobsById[myJobId]) +} + func TestGetStreamFilename(tester *testing.T) { - ds := NewFileDatastoreImpl() + ds := NewFileDatastoreImpl(fake.NewUnauthorizedServer()) cfg := make(module.ModuleConfig) cfg["jobDir"] = "/tmp/jobs" ds.Init(cfg) - filename := ds.getStreamFilename(ds.CreateJob()) + filename := ds.getStreamFilename(ds.CreateJob(newContext())) assert.Equal(tester, "/tmp/jobs/1001.bin", filename) } diff --git a/server/modules/statickeyauth/statickeyauthimpl.go b/server/modules/statickeyauth/statickeyauthimpl.go index 3d8ee442..7e106616 100644 --- a/server/modules/statickeyauth/statickeyauthimpl.go +++ b/server/modules/statickeyauth/statickeyauthimpl.go @@ -13,11 +13,12 @@ package statickeyauth import ( "context" "errors" + "github.com/apex/log" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/web" "net" "net/http" "strings" - "github.com/apex/log" - "github.com/security-onion-solutions/securityonion-soc/web" ) type StaticKeyAuthImpl struct { @@ -26,8 +27,7 @@ type StaticKeyAuthImpl struct { } func NewStaticKeyAuthImpl() *StaticKeyAuthImpl { - return &StaticKeyAuthImpl{ - } + return &StaticKeyAuthImpl{} } func (auth *StaticKeyAuthImpl) Init(apiKey string, anonymousCidr string) error { @@ -49,7 +49,11 @@ func (auth *StaticKeyAuthImpl) Preprocess(ctx context.Context, req *http.Request statusCode = http.StatusUnauthorized err = errors.New("Access denied") } else { - ctx = context.WithValue(ctx, web.ContextKeyRequestor, "SONODE") + // Currently all static auth clients are sensors + sensorUser := model.NewUser() + sensorUser.Id = "sensor" + sensorUser.Email = "sensor" + ctx = context.WithValue(ctx, web.ContextKeyRequestor, sensorUser) } return ctx, statusCode, err } diff --git a/server/modules/statickeyauth/statickeyauthimpl_test.go b/server/modules/statickeyauth/statickeyauthimpl_test.go index aece97aa..39baba2a 100644 --- a/server/modules/statickeyauth/statickeyauthimpl_test.go +++ b/server/modules/statickeyauth/statickeyauthimpl_test.go @@ -15,6 +15,7 @@ import ( "net/http" "testing" + "github.com/security-onion-solutions/securityonion-soc/model" "github.com/security-onion-solutions/securityonion-soc/web" "github.com/stretchr/testify/assert" ) @@ -79,8 +80,10 @@ func TestPreprocess(tester *testing.T) { if assert.NotNil(tester, ctx) { requestor := ctx.Value(web.ContextKeyRequestor) if assert.NotNil(tester, requestor) { - actualId := requestor.(string) - assert.Equal(tester, "SONODE", actualId) + sensorUser := requestor.(*model.User) + assert.NotNil(tester, sensorUser) + assert.Equal(tester, "sensor", sensorUser.Id) + assert.Equal(tester, "sensor", sensorUser.Email) } } } diff --git a/server/modules/staticrbac/staticrbacauthorizer.go b/server/modules/staticrbac/staticrbacauthorizer.go index 004e3f35..e83a5110 100644 --- a/server/modules/staticrbac/staticrbacauthorizer.go +++ b/server/modules/staticrbac/staticrbacauthorizer.go @@ -125,23 +125,22 @@ func (impl *StaticRbacAuthorizer) CheckContextOperationAuthorized(ctx context.Co permission := target + "/" + operation if user, ok := ctx.Value(web.ContextKeyRequestor).(*model.User); ok { - log.WithFields(log.Fields{ - "userId": user.Id, - "username": user.Email, - "requestId": ctx.Value(web.ContextKeyRequestId), - "permission": permission, - "roleMap": impl.roleMap, - }).Info("Evaluating authorization for requestor") - impl.mutex.Lock() defer impl.mutex.Unlock() if user.Email == permission { err = errors.New("Unable to check authorization of a subject name that matches the permission name itself") - } - if !impl.isAuthorized(user.Email, permission) { + } else if !impl.isAuthorized(user.Email, permission) { err = model.NewUnauthorized(user.Email, operation, target) } + log.WithFields(log.Fields{ + "userId": user.Id, + "username": user.Email, + "requestId": ctx.Value(web.ContextKeyRequestId), + "permission": permission, + "primaryRoles": impl.roleMap[user.Email], + "authorized": err == nil, + }).Info("Evaluating authorization for requestor") } else { log.Debug("Authorization user not found in context") err = model.NewUnauthorized("", operation, target) @@ -196,7 +195,7 @@ func (impl *StaticRbacAuthorizer) scanFiles() { hash := md5.Sum([]byte(hashText)) if hash != impl.previousHash { - log.Info("Role files have changed; updating roles") + log.WithField("roleMap", newRoleMap).Info("Role files have changed; updating roles") impl.UpdateRoleMap(newRoleMap) impl.previousHash = hash } diff --git a/server/nodehandler.go b/server/nodehandler.go index cd888f2e..fb37893c 100644 --- a/server/nodehandler.go +++ b/server/nodehandler.go @@ -13,18 +13,18 @@ package server import ( "context" "errors" - "net/http" "github.com/security-onion-solutions/securityonion-soc/model" "github.com/security-onion-solutions/securityonion-soc/web" + "net/http" ) type NodeHandler struct { web.BaseHandler - server *Server + server *Server } func NewNodeHandler(srv *Server) *NodeHandler { - handler := &NodeHandler {} + handler := &NodeHandler{} handler.Host = srv.Host handler.server = srv handler.Impl = handler @@ -33,7 +33,8 @@ func NewNodeHandler(srv *Server) *NodeHandler { 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(ctx, writer, request) + case http.MethodPost: + return nodeHandler.post(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } @@ -47,8 +48,8 @@ func (nodeHandler *NodeHandler) post(ctx context.Context, writer http.ResponseWr if err == nil { nodeHandler.server.Metrics.UpdateNodeMetrics(node) nodeHandler.Host.Broadcast("node", node) - job = nodeHandler.server.Datastore.GetNextJob(node.Id) + job = nodeHandler.server.Datastore.GetNextJob(ctx, node.Id) } } return http.StatusOK, job, err -} \ No newline at end of file +} diff --git a/server/packethandler.go b/server/packethandler.go index 8e64fc8a..e3770a9e 100644 --- a/server/packethandler.go +++ b/server/packethandler.go @@ -13,18 +13,18 @@ package server import ( "context" "errors" + "github.com/security-onion-solutions/securityonion-soc/web" "net/http" "strconv" - "github.com/security-onion-solutions/securityonion-soc/web" ) type PacketHandler struct { web.BaseHandler - server *Server + server *Server } func NewPacketHandler(srv *Server) *PacketHandler { - handler := &PacketHandler {} + handler := &PacketHandler{} handler.Host = srv.Host handler.server = srv handler.Impl = handler @@ -33,7 +33,8 @@ func NewPacketHandler(srv *Server) *PacketHandler { 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(ctx, writer, request) + case http.MethodGet: + return packetHandler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } @@ -60,7 +61,7 @@ func (packetHandler *PacketHandler) get(ctx context.Context, writer http.Respons count = tmpCount } } - packets, err := packetHandler.server.Datastore.GetPackets(int(jobId), int(offset), count, unwrap) + packets, err := packetHandler.server.Datastore.GetPackets(ctx, int(jobId), int(offset), count, unwrap) if err == nil { statusCode = http.StatusOK } else { diff --git a/server/streamhandler.go b/server/streamhandler.go index f332b593..7f43ed12 100644 --- a/server/streamhandler.go +++ b/server/streamhandler.go @@ -13,22 +13,22 @@ package server import ( "context" "errors" + "github.com/apex/log" + "github.com/security-onion-solutions/securityonion-soc/web" "io" "net/http" "regexp" "strconv" "strings" - "github.com/apex/log" - "github.com/security-onion-solutions/securityonion-soc/web" ) type StreamHandler struct { web.BaseHandler - server *Server + server *Server } func NewStreamHandler(srv *Server) *StreamHandler { - handler := &StreamHandler {} + handler := &StreamHandler{} handler.Host = srv.Host handler.server = srv handler.Impl = handler @@ -37,8 +37,10 @@ func NewStreamHandler(srv *Server) *StreamHandler { 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(ctx, writer, request) - case http.MethodPost: return streamHandler.post(ctx, 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") } @@ -53,7 +55,7 @@ func (streamHandler *StreamHandler) get(ctx context.Context, writer http.Respons if err != nil { return statusCode, nil, err } - reader, filename, length, err := streamHandler.server.Datastore.GetPacketStream(int(jobId), unwrap) + reader, filename, length, err := streamHandler.server.Datastore.GetPacketStream(ctx, int(jobId), unwrap) extension := request.URL.Query().Get("ext") if len(extension) > 0 { safe, _ := regexp.MatchString(`^[a-zA-Z0-9-_]+$`, extension) @@ -71,15 +73,15 @@ func (streamHandler *StreamHandler) get(ctx context.Context, writer http.Respons statusCode = http.StatusOK writer.Header().Set("Content-Type", "vnd.tcpdump.pcap") writer.Header().Set("Content-Length", strconv.FormatInt(length, 10)) - writer.Header().Set("Content-Disposition", "inline; filename=\"" + filename + "\""); - writer.Header().Set("Content-Transfer-Encoding", "binary"); + writer.Header().Set("Content-Disposition", "inline; filename=\""+filename+"\"") + writer.Header().Set("Content-Transfer-Encoding", "binary") written, err := io.Copy(writer, reader) if err != nil { - log.WithError(err).WithFields(log.Fields { + log.WithError(err).WithFields(log.Fields{ "name": filename, }).Error("Failed to copy stream") } - log.WithFields(log.Fields { + log.WithFields(log.Fields{ "name": filename, "size": written, }).Info("Copied stream to response") @@ -92,7 +94,7 @@ func (streamHandler *StreamHandler) get(ctx context.Context, writer http.Respons 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) + err = streamHandler.server.Datastore.SavePacketStream(ctx, int(jobId), request.Body) if err == nil { statusCode = http.StatusOK } diff --git a/server/userstore.go b/server/userstore.go index c8d71e29..e4fa3c17 100644 --- a/server/userstore.go +++ b/server/userstore.go @@ -20,4 +20,4 @@ type Userstore interface { DeleteUser(id string) error GetUser(ctx context.Context, id string) (*model.User, error) UpdateUser(id string, user *model.User) error -} \ No newline at end of file +} From b2a4ec32e65c8aaaacf8490431c290ccdc88980d Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Wed, 15 Sep 2021 07:37:48 -0400 Subject: [PATCH 14/32] Refactor datastore API to avoid requiring unnecessary permissions --- server/datastore.go | 2 +- server/jobhandler.go | 29 +++----- .../filedatastore/filedatastoreimpl.go | 68 ++++++++++--------- .../filedatastore/filedatastoreimpl_test.go | 18 ++--- 4 files changed, 56 insertions(+), 61 deletions(-) diff --git a/server/datastore.go b/server/datastore.go index d55c0b34..10775cfe 100644 --- a/server/datastore.go +++ b/server/datastore.go @@ -27,7 +27,7 @@ type Datastore interface { GetJobs(ctx context.Context) []*model.Job AddJob(ctx context.Context, job *model.Job) error UpdateJob(ctx context.Context, job *model.Job) error - DeleteJob(ctx context.Context, job *model.Job) error + DeleteJob(ctx context.Context, jobId int) (*model.Job, error) GetPackets(ctx context.Context, jobId int, offset int, count int, unwrap bool) ([]*model.Packet, error) SavePacketStream(ctx context.Context, jobId int, reader io.ReadCloser) error GetPacketStream(ctx context.Context, jobId int, unwrap bool) (io.ReadCloser, string, int64, error) diff --git a/server/jobhandler.go b/server/jobhandler.go index d6b1d166..f46398ce 100644 --- a/server/jobhandler.go +++ b/server/jobhandler.go @@ -83,17 +83,10 @@ func (jobHandler *JobHandler) put(ctx context.Context, writer http.ResponseWrite job := model.NewJob() err := jobHandler.ReadJson(request, job) if err == nil { - existingJob := jobHandler.server.Datastore.GetJob(ctx, job.Id) - if existingJob != nil { - job.UserId = existingJob.UserId // Prevent users from altering the creating user - - err = jobHandler.server.Datastore.UpdateJob(ctx, job) - if err == nil { - jobHandler.Host.Broadcast("job", job) - statusCode = http.StatusOK - } else { - statusCode = http.StatusNotFound - } + err = jobHandler.server.Datastore.UpdateJob(ctx, job) + if err == nil { + jobHandler.Host.Broadcast("job", job) + statusCode = http.StatusOK } else { statusCode = http.StatusNotFound } @@ -113,15 +106,11 @@ func (jobHandler *JobHandler) delete(ctx context.Context, writer http.ResponseWr jobId, err := strconv.Atoi(id) if err == nil { - job := jobHandler.server.Datastore.GetJob(ctx, int(jobId)) - if job != nil { - err = jobHandler.server.Datastore.DeleteJob(ctx, job) - if err == nil { - jobHandler.Host.Broadcast("job", job) - statusCode = http.StatusOK - } - } else { - statusCode = http.StatusNotFound + var job *model.Job + job, err = jobHandler.server.Datastore.DeleteJob(ctx, int(jobId)) + if err == nil { + jobHandler.Host.Broadcast("job", job) + statusCode = http.StatusOK } } diff --git a/server/modules/filedatastore/filedatastoreimpl.go b/server/modules/filedatastore/filedatastoreimpl.go index 7595b2d0..bd48639e 100644 --- a/server/modules/filedatastore/filedatastoreimpl.go +++ b/server/modules/filedatastore/filedatastoreimpl.go @@ -224,20 +224,26 @@ func (datastore *FileDatastoreImpl) AddJob(ctx context.Context, job *model.Job) func (datastore *FileDatastoreImpl) UpdateJob(ctx context.Context, job *model.Job) error { var err error if err = datastore.server.Authorizer.CheckContextOperationAuthorized(ctx, "process", "jobs"); err == nil { - if job.CanProcess() { - datastore.lock.Lock() - defer datastore.lock.Unlock() - err = datastore.deleteJob(job) - if err == nil { + existingJob := datastore.getJobById(job.Id) + if existingJob != nil { + job.UserId = existingJob.UserId // Prevent users from altering the creating user + job.NodeId = existingJob.NodeId // Do not allow moving a job between nodes due to data file path + if job.CanProcess() { + datastore.lock.Lock() + defer datastore.lock.Unlock() + datastore.deleteJob(existingJob) err = datastore.addJob(job) if err == nil { err = datastore.saveJob(job) } + } else { + err = errors.New("Job is ineligible for processing") } } else { - err = errors.New("Job is ineligible for processing") + err = errors.New("Job not found") } } + return err } @@ -245,11 +251,14 @@ func (datastore *FileDatastoreImpl) getJobById(jobId int) *model.Job { return datastore.jobsById[jobId] } -func (datastore *FileDatastoreImpl) DeleteJob(ctx context.Context, job *model.Job) error { +func (datastore *FileDatastoreImpl) DeleteJob(ctx context.Context, jobId int) (*model.Job, error) { var err error - if datastore.jobIsAllowed(ctx, job, "delete") { - err = datastore.deleteJob(job) - if err == nil { + job := datastore.getJobById(jobId) + if job != nil { + if datastore.jobIsAllowed(ctx, job, "delete") { + datastore.lock.Lock() + defer datastore.lock.Unlock() + datastore.deleteJob(job) job.Status = model.JobStatusDeleted filename := fmt.Sprintf("%d.json", job.Id) folder := filepath.Join(datastore.jobDir, sanitize.Name(job.GetNodeId())) @@ -266,32 +275,29 @@ func (datastore *FileDatastoreImpl) DeleteJob(ctx context.Context, job *model.Jo "filename": filename, }).Info("Permanently deleted job and job files") } + } else { + err = errors.New("Permission denied attempting to delete job") } + } else { + err = errors.New("Job not found") } - return err + return job, err } -func (datastore *FileDatastoreImpl) deleteJob(job *model.Job) error { - var err error - existingJob := datastore.getJobById(job.Id) - if existingJob == nil { - err = errors.New("Job does not exist") - } else { - jobs := datastore.jobsByNodeId[job.GetNodeId()] - newJobs := make([]*model.Job, 0) - for _, currentJob := range jobs { - if currentJob.Id != job.Id { - newJobs = append(newJobs, currentJob) - } +func (datastore *FileDatastoreImpl) deleteJob(job *model.Job) { + jobs := datastore.jobsByNodeId[job.GetNodeId()] + newJobs := make([]*model.Job, 0) + for _, currentJob := range jobs { + if currentJob.Id != job.Id { + newJobs = append(newJobs, currentJob) } - datastore.jobsByNodeId[job.GetNodeId()] = newJobs - delete(datastore.jobsById, job.Id) - log.WithFields(log.Fields{ - "id": job.Id, - "node": job.GetNodeId(), - }).Debug("Deleted job from list") } - return err + datastore.jobsByNodeId[job.GetNodeId()] = newJobs + delete(datastore.jobsById, job.Id) + log.WithFields(log.Fields{ + "id": job.Id, + "node": job.GetNodeId(), + }).Debug("Deleted job from list") } func (datastore *FileDatastoreImpl) addJob(job *model.Job) error { @@ -384,7 +390,7 @@ func (datastore *FileDatastoreImpl) GetPackets(ctx context.Context, jobId int, o func (datastore *FileDatastoreImpl) SavePacketStream(ctx context.Context, jobId int, reader io.ReadCloser) error { var err error if err = datastore.server.Authorizer.CheckContextOperationAuthorized(ctx, "process", "jobs"); err == nil { - job := datastore.GetJob(ctx, jobId) + job := datastore.getJobById(jobId) if job != nil { if job.CanProcess() { var count int64 diff --git a/server/modules/filedatastore/filedatastoreimpl_test.go b/server/modules/filedatastore/filedatastoreimpl_test.go index c19e745f..c444703c 100644 --- a/server/modules/filedatastore/filedatastoreimpl_test.go +++ b/server/modules/filedatastore/filedatastoreimpl_test.go @@ -118,12 +118,12 @@ func TestJobReadAuthorization(tester *testing.T) { ds.addNode(node) myJobId := 10001 - anothersJobId := 10002 + anotherJobId := 10002 // Test adding a job job := ds.CreateJob(newContext()) job.UserId = ANOTHER_USER_ID - job.Id = anothersJobId + job.Id = anotherJobId ds.addJob(job) job = ds.CreateJob(newContext()) @@ -134,7 +134,7 @@ func TestJobReadAuthorization(tester *testing.T) { job = ds.GetJob(newContext(), myJobId) assert.Equal(tester, myJobId, job.Id) - job = ds.GetJob(newContext(), anothersJobId) + job = ds.GetJob(newContext(), anotherJobId) assert.Nil(tester, job) // Test fetching all jobs @@ -150,12 +150,12 @@ func TestJobDeleteAuthorization(tester *testing.T) { ds.addNode(node) myJobId := 10001 - anothersJobId := 10002 + anotherJobId := 10002 // Test adding a job anotherJob := ds.CreateJob(newContext()) anotherJob.UserId = ANOTHER_USER_ID - anotherJob.Id = anothersJobId + anotherJob.Id = anotherJobId ds.addJob(anotherJob) myJob := ds.CreateJob(newContext()) @@ -164,14 +164,14 @@ func TestJobDeleteAuthorization(tester *testing.T) { ds.addJob(myJob) assert.NotNil(tester, ds.jobsById[myJobId]) - assert.NotNil(tester, ds.jobsById[anothersJobId]) + assert.NotNil(tester, ds.jobsById[anotherJobId]) // Should not delete another user's job - ds.DeleteJob(newContext(), anotherJob) - assert.NotNil(tester, ds.jobsById[anothersJobId]) + ds.DeleteJob(newContext(), anotherJobId) + assert.NotNil(tester, ds.jobsById[anotherJobId]) // Should delete my own job - ds.DeleteJob(newContext(), myJob) + ds.DeleteJob(newContext(), myJobId) assert.Nil(tester, ds.jobsById[myJobId]) } From bfc9a125802bd47000b93d3ae4822ce19066f8d8 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Wed, 15 Sep 2021 08:26:56 -0400 Subject: [PATCH 15/32] Check existing job's eligibility to be processed, not the incoming job --- .../filedatastore/filedatastoreimpl.go | 2 +- .../filedatastore/filedatastoreimpl_test.go | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/server/modules/filedatastore/filedatastoreimpl.go b/server/modules/filedatastore/filedatastoreimpl.go index bd48639e..757d2491 100644 --- a/server/modules/filedatastore/filedatastoreimpl.go +++ b/server/modules/filedatastore/filedatastoreimpl.go @@ -228,7 +228,7 @@ func (datastore *FileDatastoreImpl) UpdateJob(ctx context.Context, job *model.Jo if existingJob != nil { job.UserId = existingJob.UserId // Prevent users from altering the creating user job.NodeId = existingJob.NodeId // Do not allow moving a job between nodes due to data file path - if job.CanProcess() { + if existingJob.CanProcess() { datastore.lock.Lock() defer datastore.lock.Unlock() datastore.deleteJob(existingJob) diff --git a/server/modules/filedatastore/filedatastoreimpl_test.go b/server/modules/filedatastore/filedatastoreimpl_test.go index c444703c..13f7e6e9 100644 --- a/server/modules/filedatastore/filedatastoreimpl_test.go +++ b/server/modules/filedatastore/filedatastoreimpl_test.go @@ -183,3 +183,39 @@ func TestGetStreamFilename(tester *testing.T) { filename := ds.getStreamFilename(ds.CreateJob(newContext())) assert.Equal(tester, "/tmp/jobs/1001.bin", filename) } + +func TestUpdateInelegible(tester *testing.T) { + ds := NewFileDatastoreImpl(fake.NewUnauthorizedServer()) + cfg := make(module.ModuleConfig) + ds.Init(cfg) + + job := ds.CreateJob(newContext()) + job.UserId = MY_USER_ID // This user's job + job.Id = 1212 + ds.addJob(job) + + err := ds.UpdateJob(newContext(), job) + assert.Error(tester, err, "Job is inelegible for processing") +} + +func TestUpdatePreserveData(tester *testing.T) { + ds := NewFileDatastoreImpl(fake.NewAuthorizedServer(nil)) + cfg := make(module.ModuleConfig) + ds.Init(cfg) + + job := ds.CreateJob(newContext()) + job.UserId = MY_USER_ID // This user's job + job.NodeId = "some node" + job.Id = 1212 + job.Status = model.JobStatusPending + ds.addJob(job) + + newJob := ds.CreateJob(newContext()) + newJob.Id = job.Id + newJob.UserId = ANOTHER_USER_ID + newJob.NodeId = "some other node" + err := ds.UpdateJob(newContext(), newJob) + assert.NoError(tester, err) + assert.Equal(tester, job.UserId, newJob.UserId) + assert.Equal(tester, job.NodeId, newJob.NodeId) +} From 68887d1a7908882ced620b6be943c4e90f3bbe91 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Wed, 15 Sep 2021 19:46:56 -0400 Subject: [PATCH 16/32] Continued auth enhancements --- rbac/permissions | 23 ++- rbac/roles | 12 +- server/casehandler.go | 13 +- server/casestore.go | 5 +- server/datastore.go | 9 +- server/gridhandler.go | 13 +- server/jobhandler.go | 13 +- server/metrics.go | 5 +- server/modules/elastic/elastic.go | 10 +- server/modules/elastic/elasticeventstore.go | 186 ++++++++++-------- server/modules/elastic/joblookuphandler.go | 18 +- .../filedatastore/filedatastoreimpl.go | 96 +++++---- .../filedatastore/filedatastoreimpl_test.go | 154 ++++++++++----- server/modules/influxdb/influxdb.go | 34 ++-- server/modules/influxdb/influxdbmetrics.go | 38 ++-- .../modules/influxdb/influxdbmetrics_test.go | 22 ++- server/modules/kratos/kratosuserstore.go | 32 +-- server/modules/sostatus/sostatus.go | 111 ++++++----- server/modules/sostatus/sostatus_test.go | 6 +- server/modules/statickeyauth/statickeyauth.go | 2 +- .../statickeyauth/statickeyauthimpl.go | 18 +- .../statickeyauth/statickeyauthimpl_test.go | 15 +- server/modules/thehive/thehive.go | 12 +- server/modules/thehive/thehivecasestore.go | 50 +++-- .../modules/thehive/thehivecasestore_test.go | 45 +++++ server/nodehandler.go | 4 +- server/server.go | 14 +- server/userhandler.go | 43 ++-- server/usershandler.go | 19 +- server/userstore.go | 4 +- 30 files changed, 613 insertions(+), 413 deletions(-) create mode 100644 server/modules/thehive/thehivecasestore_test.go diff --git a/rbac/permissions b/rbac/permissions index c7f8b906..a66ecf73 100644 --- a/rbac/permissions +++ b/rbac/permissions @@ -2,21 +2,30 @@ # Syntax => permX: roleY roleZ # Explanation => roleY and roleZ are granted permission permX -grid/read: grid-monitor -roles/read: user-monitor -roles/write: user-admin -users/read: user-monitor -users/write: user-admin +cases/write: case-admin +events/read: event-monitor +events/write: event-admin +events/ack: event-admin jobs/read: job-monitor +jobs/pivot: job-user jobs/write: job-admin jobs/delete: job-admin jobs/process: job-processor +nodes/read: node-monitor +nodes/write: node-admin +roles/read: user-monitor +roles/write: user-admin +users/read: user-monitor +users/write: user-admin +users/delete: user-admin # Define low-level permission set inheritence relationships # Syntax => roleB: roleA # Explanation => roleA inherits all of roleA's permissions -user-monitor: user-admin +event-monitor: event-admin job-monitor: job-admin -grid-monitor: grid-admin \ No newline at end of file +job-user: job-admin +node-monitor: node-admin +user-monitor: user-admin diff --git a/rbac/roles b/rbac/roles index a4af28ad..56d813bc 100644 --- a/rbac/roles +++ b/rbac/roles @@ -2,10 +2,14 @@ # Syntax => roleX: roleY roleZ # Explanation => roleY and roleZ are granted permissions of roleX -grid-admin: superuser -grid-monitor: analyst auditor +case-admin: analyst limited-analyst superuser +event-admin: analyst limited-analyst superuser +event-monitor: auditor limited-auditor +node-admin: agent superuser +node-monitor: analyst limited-analyst auditor user-admin: superuser user-monitor: analyst auditor -job-admin: superuser analyst +job-admin: analyst superuser +job-user: limited-analyst job-monitor: auditor -job-processor: sensor +job-processor: agent diff --git a/server/casehandler.go b/server/casehandler.go index d8a9e280..0edba00c 100644 --- a/server/casehandler.go +++ b/server/casehandler.go @@ -11,20 +11,20 @@ package server import ( "context" - "errors" "encoding/json" - "net/http" + "errors" "github.com/security-onion-solutions/securityonion-soc/model" "github.com/security-onion-solutions/securityonion-soc/web" + "net/http" ) type CaseHandler struct { web.BaseHandler - server *Server + server *Server } func NewCaseHandler(srv *Server) *CaseHandler { - handler := &CaseHandler {} + handler := &CaseHandler{} handler.Host = srv.Host handler.server = srv handler.Impl = handler @@ -38,7 +38,8 @@ func (caseHandler *CaseHandler) HandleNow(ctx context.Context, writer http.Respo if caseHandler.server.Casestore != nil { switch request.Method { - case http.MethodPost: return caseHandler.create(ctx, writer, request) + case http.MethodPost: + return caseHandler.create(ctx, writer, request) } } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") @@ -51,7 +52,7 @@ func (caseHandler *CaseHandler) create(ctx context.Context, writer http.Response inputCase := model.NewCase() err := json.NewDecoder(request.Body).Decode(&inputCase) if err == nil { - outputCase, err = caseHandler.server.Casestore.Create(inputCase) + outputCase, err = caseHandler.server.Casestore.Create(ctx, inputCase) if err == nil { statusCode = http.StatusOK } else { diff --git a/server/casestore.go b/server/casestore.go index de86411b..b3c0e416 100644 --- a/server/casestore.go +++ b/server/casestore.go @@ -10,9 +10,10 @@ package server import ( + "context" "github.com/security-onion-solutions/securityonion-soc/model" ) type Casestore interface { - Create(newCase *model.Case) (*model.Case, error) -} \ No newline at end of file + Create(ctx context.Context, newCase *model.Case) (*model.Case, error) +} diff --git a/server/datastore.go b/server/datastore.go index 10775cfe..da188dda 100644 --- a/server/datastore.go +++ b/server/datastore.go @@ -17,15 +17,16 @@ import ( ) type Datastore interface { - CreateNode(id string) *model.Node - GetNodes() []*model.Node - AddNode(node *model.Node) error - UpdateNode(newNode *model.Node) (*model.Node, error) + CreateNode(ctx context.Context, id string) *model.Node + GetNodes(ctx context.Context) []*model.Node + AddNode(ctx context.Context, node *model.Node) error + UpdateNode(ctx context.Context, newNode *model.Node) (*model.Node, error) GetNextJob(ctx context.Context, nodeId string) *model.Job CreateJob(ctx context.Context) *model.Job GetJob(ctx context.Context, jobId int) *model.Job GetJobs(ctx context.Context) []*model.Job AddJob(ctx context.Context, job *model.Job) error + AddPivotJob(ctx context.Context, job *model.Job) error UpdateJob(ctx context.Context, job *model.Job) error DeleteJob(ctx context.Context, jobId int) (*model.Job, error) GetPackets(ctx context.Context, jobId int, offset int, count int, unwrap bool) ([]*model.Packet, error) diff --git a/server/gridhandler.go b/server/gridhandler.go index ec9ad71c..eeb77e7c 100644 --- a/server/gridhandler.go +++ b/server/gridhandler.go @@ -13,17 +13,17 @@ package server import ( "context" "errors" - "net/http" "github.com/security-onion-solutions/securityonion-soc/web" + "net/http" ) type GridHandler struct { web.BaseHandler - server *Server + server *Server } func NewGridHandler(srv *Server) *GridHandler { - handler := &GridHandler {} + handler := &GridHandler{} handler.Host = srv.Host handler.server = srv handler.Impl = handler @@ -32,11 +32,12 @@ func NewGridHandler(srv *Server) *GridHandler { 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(ctx, writer, request) + case http.MethodGet: + return gridHandler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } 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 + return http.StatusOK, gridHandler.server.Datastore.GetNodes(ctx), nil +} diff --git a/server/jobhandler.go b/server/jobhandler.go index f46398ce..be36a44d 100644 --- a/server/jobhandler.go +++ b/server/jobhandler.go @@ -64,15 +64,10 @@ func (jobHandler *JobHandler) post(ctx context.Context, writer http.ResponseWrit job := jobHandler.server.Datastore.CreateJob(ctx) err := jobHandler.ReadJson(request, job) if err == nil { - if user, ok := ctx.Value(web.ContextKeyRequestor).(*model.User); ok { - job.UserId = user.Id - err = jobHandler.server.Datastore.AddJob(ctx, job) - if err == nil { - jobHandler.Host.Broadcast("job", job) - statusCode = http.StatusCreated - } - } else { - err = errors.New("User not found in context") + err = jobHandler.server.Datastore.AddJob(ctx, job) + if err == nil { + jobHandler.Host.Broadcast("job", job) + statusCode = http.StatusCreated } } return statusCode, job, err diff --git a/server/metrics.go b/server/metrics.go index 35907013..2e864b90 100644 --- a/server/metrics.go +++ b/server/metrics.go @@ -11,10 +11,11 @@ package server import ( + "context" "github.com/security-onion-solutions/securityonion-soc/model" ) type Metrics interface { - GetGridEps() int - UpdateNodeMetrics(node *model.Node) bool + GetGridEps(ctx context.Context) int + UpdateNodeMetrics(ctx context.Context, node *model.Node) bool } diff --git a/server/modules/elastic/elastic.go b/server/modules/elastic/elastic.go index 7608223e..f2f707a1 100644 --- a/server/modules/elastic/elastic.go +++ b/server/modules/elastic/elastic.go @@ -25,15 +25,15 @@ const DEFAULT_ASYNC_THRESHOLD = 10 const DEFAULT_INTERVALS = 25 type Elastic struct { - config module.ModuleConfig - server *server.Server - store *ElasticEventstore + config module.ModuleConfig + server *server.Server + store *ElasticEventstore } func NewElastic(srv *server.Server) *Elastic { - return &Elastic { + return &Elastic{ server: srv, - store: NewElasticEventstore(), + store: NewElasticEventstore(srv), } } diff --git a/server/modules/elastic/elasticeventstore.go b/server/modules/elastic/elasticeventstore.go index f7bc38b3..bbfe036f 100644 --- a/server/modules/elastic/elasticeventstore.go +++ b/server/modules/elastic/elasticeventstore.go @@ -26,6 +26,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/server" "github.com/security-onion-solutions/securityonion-soc/web" "github.com/tidwall/gjson" ) @@ -40,6 +41,7 @@ type FieldDefinition struct { } type ElasticEventstore struct { + server *server.Server hostUrls []string esClient *elasticsearch.Client esRemoteClients []*elasticsearch.Client @@ -57,8 +59,9 @@ type ElasticEventstore struct { asyncThreshold int } -func NewElasticEventstore() *ElasticEventstore { +func NewElasticEventstore(srv *server.Server) *ElasticEventstore { return &ElasticEventstore{ + server: srv, hostUrls: make([]string, 0), esRemoteClients: make([]*elasticsearch.Client, 0), esAllClients: make([]*elasticsearch.Client, 0), @@ -165,19 +168,22 @@ func (store *ElasticEventstore) unmapElasticField(field string) string { } func (store *ElasticEventstore) Search(ctx context.Context, criteria *model.EventSearchCriteria) (*model.EventSearchResults, error) { - store.refreshCache(ctx) - + var err error results := model.NewEventSearchResults() - query, err := convertToElasticRequest(store, criteria) - if err == nil { - var response string - response, err = store.luceneSearch(ctx, query) + if err := store.server.Authorizer.CheckContextOperationAuthorized(ctx, "read", "events"); err == nil { + store.refreshCache(ctx) + + var query string + query, err = convertToElasticRequest(store, criteria) if err == nil { - err = convertFromElasticResults(store, response, results) - results.Criteria = criteria + var response string + response, err = store.luceneSearch(ctx, query) + if err == nil { + err = convertFromElasticResults(store, response, results) + results.Criteria = criteria + } } } - results.Complete() return results, err } @@ -193,39 +199,43 @@ func (store *ElasticEventstore) disableCrossClusterIndexing(indexes []string) [] } func (store *ElasticEventstore) Update(ctx context.Context, criteria *model.EventUpdateCriteria) (*model.EventUpdateResults, error) { - store.refreshCache(ctx) - + var err error results := model.NewEventUpdateResults() - results.Criteria = criteria - query, err := convertToElasticUpdateRequest(store, criteria) - if err == nil { - var response string + if err = store.server.Authorizer.CheckContextOperationAuthorized(ctx, "write", "events"); err == nil { + store.refreshCache(ctx) - for idx, client := range store.esAllClients { - log.WithField("clientHost", store.hostUrls[idx]).Debug("Sending request to client") - response, err = store.updateDocuments(ctx, client, query, store.disableCrossClusterIndexing(strings.Split(store.index, ",")), !criteria.Asynchronous) - if err == nil { - if !criteria.Asynchronous { - currentResults := model.NewEventUpdateResults() - err = convertFromElasticUpdateResults(store, response, currentResults) - if err == nil { - results.AddEventUpdateResults(currentResults) - } else { - log.WithError(err).WithField("clientHost", store.hostUrls[idx]).Error("Encountered error while updating elasticsearch") - results.Errors = append(results.Errors, err.Error()) + results.Criteria = criteria + var query string + query, err = convertToElasticUpdateRequest(store, criteria) + if err == nil { + var response string + + for idx, client := range store.esAllClients { + log.WithField("clientHost", store.hostUrls[idx]).Debug("Sending request to client") + response, err = store.updateDocuments(ctx, client, query, store.disableCrossClusterIndexing(strings.Split(store.index, ",")), !criteria.Asynchronous) + if err == nil { + if !criteria.Asynchronous { + currentResults := model.NewEventUpdateResults() + err = convertFromElasticUpdateResults(store, response, currentResults) + if err == nil { + results.AddEventUpdateResults(currentResults) + } else { + log.WithError(err).WithField("clientHost", store.hostUrls[idx]).Error("Encountered error while updating elasticsearch") + results.Errors = append(results.Errors, err.Error()) + } } + } else { + log.WithError(err).WithField("clientHost", store.hostUrls[idx]).Error("Encountered error while updating elasticsearch") + results.Errors = append(results.Errors, err.Error()) } - } else { - log.WithError(err).WithField("clientHost", store.hostUrls[idx]).Error("Encountered error while updating elasticsearch") - results.Errors = append(results.Errors, err.Error()) } } - } - if len(results.Errors) < len(store.esAllClients) { - // Do not fail this request completely since some hosts succeeded. - // The results.Errors property contains the list of errors. - err = nil + if len(results.Errors) < len(store.esAllClients) { + // Do not fail this request completely since some hosts succeeded. + // The results.Errors property contains the list of errors. + err = nil + } } results.Complete() @@ -693,61 +703,63 @@ func (store *ElasticEventstore) Acknowledge(ctx context.Context, ackCriteria *mo var results *model.EventUpdateResults var err error if len(ackCriteria.EventFilter) > 0 { - log.WithFields(log.Fields{ - "searchFilter": ackCriteria.SearchFilter, - "eventFilter": ackCriteria.EventFilter, - "escalate": ackCriteria.Escalate, - "acknowledge": ackCriteria.Acknowledge, - "requestId": ctx.Value(web.ContextKeyRequestId), - }).Info("Acknowledging event") - - updateCriteria := model.NewEventUpdateCriteria() - updateCriteria.AddUpdateScript("ctx._source.event.acknowledged=" + strconv.FormatBool(ackCriteria.Acknowledge)) - if ackCriteria.Escalate && ackCriteria.Acknowledge { - updateCriteria.AddUpdateScript("ctx._source.event.escalated=true") - } - updateCriteria.Populate(ackCriteria.SearchFilter, - ackCriteria.DateRange, - ackCriteria.DateRangeFormat, - ackCriteria.Timezone, - "0", - "0") - - // Add the event filters to the search query - var searchSegment *model.SearchSegment - segment := updateCriteria.ParsedQuery.NamedSegment("search") - if segment == nil { - searchSegment = model.NewSearchSegmentEmpty() - } else { - searchSegment = segment.(*model.SearchSegment) - } + if err = store.server.Authorizer.CheckContextOperationAuthorized(ctx, "ack", "events"); err == nil { + log.WithFields(log.Fields{ + "searchFilter": ackCriteria.SearchFilter, + "eventFilter": ackCriteria.EventFilter, + "escalate": ackCriteria.Escalate, + "acknowledge": ackCriteria.Acknowledge, + "requestId": ctx.Value(web.ContextKeyRequestId), + }).Info("Acknowledging event") + + updateCriteria := model.NewEventUpdateCriteria() + updateCriteria.AddUpdateScript("ctx._source.event.acknowledged=" + strconv.FormatBool(ackCriteria.Acknowledge)) + if ackCriteria.Escalate && ackCriteria.Acknowledge { + updateCriteria.AddUpdateScript("ctx._source.event.escalated=true") + } + updateCriteria.Populate(ackCriteria.SearchFilter, + ackCriteria.DateRange, + ackCriteria.DateRangeFormat, + ackCriteria.Timezone, + "0", + "0") + + // Add the event filters to the search query + var searchSegment *model.SearchSegment + segment := updateCriteria.ParsedQuery.NamedSegment("search") + if segment == nil { + searchSegment = model.NewSearchSegmentEmpty() + } else { + searchSegment = segment.(*model.SearchSegment) + } - updateCriteria.Asynchronous = false - for key, value := range ackCriteria.EventFilter { - if strings.ToLower(key) != "count" { - valueStr := fmt.Sprintf("%v", value) - searchSegment.AddFilter(store.mapElasticField(key), valueStr, model.IsScalar(value), true) - } else if int(value.(float64)) > store.asyncThreshold { - 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 + updateCriteria.Asynchronous = false + for key, value := range ackCriteria.EventFilter { + if strings.ToLower(key) != "count" { + valueStr := fmt.Sprintf("%v", value) + searchSegment.AddFilter(store.mapElasticField(key), valueStr, model.IsScalar(value), true) + } else if int(value.(float64)) > store.asyncThreshold { + 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 + } } - } - // Baseline the query to be based only on the search component - updateCriteria.ParsedQuery = model.NewQuery() - updateCriteria.ParsedQuery.AddSegment(searchSegment) + // Baseline the query to be based only on the search component + updateCriteria.ParsedQuery = model.NewQuery() + updateCriteria.ParsedQuery.AddSegment(searchSegment) - results, err = store.Update(ctx, updateCriteria) - if err == nil && !updateCriteria.Asynchronous { - if results.UpdatedCount == 0 { - if results.UnchangedCount == 0 { - err = errors.New("No eligible events available to acknowledge") - } else { - err = errors.New("All events have already been acknowledged") + results, err = store.Update(ctx, updateCriteria) + if err == nil && !updateCriteria.Asynchronous { + if results.UpdatedCount == 0 { + if results.UnchangedCount == 0 { + err = errors.New("No eligible events available to acknowledge") + } else { + err = errors.New("All events have already been acknowledged") + } } } } diff --git a/server/modules/elastic/joblookuphandler.go b/server/modules/elastic/joblookuphandler.go index 9d8f8f8e..f1b3365f 100644 --- a/server/modules/elastic/joblookuphandler.go +++ b/server/modules/elastic/joblookuphandler.go @@ -13,7 +13,6 @@ package elastic import ( "context" "errors" - "github.com/security-onion-solutions/securityonion-soc/model" "github.com/security-onion-solutions/securityonion-soc/server" "github.com/security-onion-solutions/securityonion-soc/web" "net/http" @@ -58,17 +57,12 @@ func (handler *JobLookupHandler) get(ctx context.Context, writer http.ResponseWr job := handler.server.Datastore.CreateJob(ctx) 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 - err = handler.server.Datastore.AddJob(ctx, 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") + err = handler.server.Datastore.AddPivotJob(ctx, 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 { statusCode = http.StatusNotFound diff --git a/server/modules/filedatastore/filedatastoreimpl.go b/server/modules/filedatastore/filedatastoreimpl.go index 757d2491..9a455bf1 100644 --- a/server/modules/filedatastore/filedatastoreimpl.go +++ b/server/modules/filedatastore/filedatastoreimpl.go @@ -65,23 +65,26 @@ func (datastore *FileDatastoreImpl) Init(cfg module.ModuleConfig) error { return datastore.loadJobs() } -func (datastore *FileDatastoreImpl) CreateNode(id string) *model.Node { - node := model.NewNode(id) +func (datastore *FileDatastoreImpl) CreateNode(ctx context.Context, id string) *model.Node { + var node *model.Node + node = model.NewNode(id) return node } -func (datastore *FileDatastoreImpl) GetNodes() []*model.Node { - datastore.lock.RLock() - defer datastore.lock.RUnlock() +func (datastore *FileDatastoreImpl) GetNodes(ctx context.Context) []*model.Node { allNodes := make([]*model.Node, 0) - for _, node := range datastore.nodesById { - allNodes = append(allNodes, node) + if err := datastore.server.Authorizer.CheckContextOperationAuthorized(ctx, "read", "nodes"); err == nil { + datastore.lock.RLock() + defer datastore.lock.RUnlock() + for _, node := range datastore.nodesById { + allNodes = append(allNodes, node) + } } return allNodes } -func (datastore *FileDatastoreImpl) AddNode(node *model.Node) error { - _, err := datastore.UpdateNode(node) +func (datastore *FileDatastoreImpl) AddNode(ctx context.Context, node *model.Node) error { + _, err := datastore.UpdateNode(ctx, node) return err } @@ -94,35 +97,37 @@ func (datastore *FileDatastoreImpl) addNode(node *model.Node) *model.Node { return node } -func (datastore *FileDatastoreImpl) UpdateNode(newNode *model.Node) (*model.Node, error) { +func (datastore *FileDatastoreImpl) UpdateNode(ctx context.Context, newNode *model.Node) (*model.Node, error) { var node *model.Node if len(newNode.Id) > 0 { - datastore.lock.Lock() - defer datastore.lock.Unlock() - node = datastore.nodesById[newNode.Id] - if node == nil { - node = datastore.addNode(newNode) - } + if err := datastore.server.Authorizer.CheckContextOperationAuthorized(ctx, "write", "nodes"); err == nil { + datastore.lock.Lock() + defer datastore.lock.Unlock() + node = datastore.nodesById[newNode.Id] + if node == nil { + node = datastore.addNode(newNode) + } - // Only copy the following values from the incoming node. Preserve everything else. - node.EpochTime = newNode.EpochTime - node.Role = newNode.Role - node.Description = newNode.Description - node.Address = newNode.Address - node.Version = newNode.Version + // Only copy the following values from the incoming node. Preserve everything else. + node.EpochTime = newNode.EpochTime + node.Role = newNode.Role + node.Description = newNode.Description + node.Address = newNode.Address + node.Version = newNode.Version - // Ensure model parameters are updated - node.SetModel(newNode.Model) + // Ensure model parameters are updated + node.SetModel(newNode.Model) - // Mark ConnectionStatus as Ok since this node just checked in - node.ConnectionStatus = model.NodeStatusOk + // Mark ConnectionStatus as Ok since this node just checked in + node.ConnectionStatus = model.NodeStatusOk - // Update time is now - node.UpdateTime = time.Now() + // Update time is now + node.UpdateTime = time.Now() - // Calculate uptime - node.UptimeSeconds = int(node.UpdateTime.Sub(node.OnlineTime).Seconds()) + // Calculate uptime + node.UptimeSeconds = int(node.UpdateTime.Sub(node.OnlineTime).Seconds()) + } } else { log.WithField("description", newNode.Description).Info("Not adding node with missing id") } @@ -211,16 +216,39 @@ func (datastore *FileDatastoreImpl) GetJobs(ctx context.Context) []*model.Job { func (datastore *FileDatastoreImpl) AddJob(ctx context.Context, job *model.Job) error { var err error if err = datastore.server.Authorizer.CheckContextOperationAuthorized(ctx, "write", "jobs"); err == nil { - datastore.lock.Lock() - defer datastore.lock.Unlock() - err = datastore.addJob(job) if err == nil { - err = datastore.saveJob(job) + err = datastore.addAndSaveJob(ctx, job) + } + } + return err +} + +func (datastore *FileDatastoreImpl) AddPivotJob(ctx context.Context, job *model.Job) error { + var err error + if err = datastore.server.Authorizer.CheckContextOperationAuthorized(ctx, "pivot", "jobs"); err == nil { + if err == nil { + err = datastore.addAndSaveJob(ctx, job) } } return err } +func (datastore *FileDatastoreImpl) addAndSaveJob(ctx context.Context, job *model.Job) error { + var err error + if user, ok := ctx.Value(web.ContextKeyRequestor).(*model.User); ok { + job.UserId = user.Id + } else { + err = errors.New("User not found in context") + } + datastore.lock.Lock() + defer datastore.lock.Unlock() + err = datastore.addJob(job) + if err == nil { + err = datastore.saveJob(job) + } + return err +} + func (datastore *FileDatastoreImpl) UpdateJob(ctx context.Context, job *model.Job) error { var err error if err = datastore.server.Authorizer.CheckContextOperationAuthorized(ctx, "process", "jobs"); err == nil { diff --git a/server/modules/filedatastore/filedatastoreimpl_test.go b/server/modules/filedatastore/filedatastoreimpl_test.go index 13f7e6e9..ff28e85c 100644 --- a/server/modules/filedatastore/filedatastoreimpl_test.go +++ b/server/modules/filedatastore/filedatastoreimpl_test.go @@ -18,12 +18,14 @@ import ( "github.com/security-onion-solutions/securityonion-soc/fake" "github.com/security-onion-solutions/securityonion-soc/model" "github.com/security-onion-solutions/securityonion-soc/module" + "github.com/security-onion-solutions/securityonion-soc/server" "github.com/security-onion-solutions/securityonion-soc/web" "github.com/stretchr/testify/assert" ) const MY_USER_ID = "123" const ANOTHER_USER_ID = "124" +const JOB_DIR = "/tmp/sensoroni.jobs" func newContext() context.Context { user := model.NewUser() @@ -31,32 +33,43 @@ func newContext() context.Context { return context.WithValue(context.Background(), web.ContextKeyRequestor, user) } -func TestFileDatastoreInit(tester *testing.T) { - ds := NewFileDatastoreImpl(fake.NewUnauthorizedServer()) - cfg := make(module.ModuleConfig) - err := ds.Init(cfg) - assert.Error(tester, err) - - jobDir := "/tmp/sensoroni.jobs" - cfg["jobDir"] = jobDir - defer os.Remove(jobDir) - os.Mkdir(jobDir, 0777) - err = ds.Init(cfg) - if assert.Nil(tester, err) { - assert.Equal(tester, DEFAULT_RETRY_FAILURE_INTERVAL_MS, ds.retryFailureIntervalMs) - } +func cleanup() { + os.RemoveAll(JOB_DIR) } -func TestNodes(tester *testing.T) { - ds := NewFileDatastoreImpl(fake.NewUnauthorizedServer()) +func createDatastore(authorized bool) (*FileDatastoreImpl, error) { + cleanup() + + var srv *server.Server + if authorized { + srv = fake.NewAuthorizedServer(nil) + } else { + srv = fake.NewUnauthorizedServer() + } + ds := NewFileDatastoreImpl(srv) cfg := make(module.ModuleConfig) - ds.Init(cfg) - node := ds.CreateNode("foo") + cfg["jobDir"] = JOB_DIR + os.MkdirAll(JOB_DIR, 0777) + err := ds.Init(cfg) + node := ds.CreateNode(newContext(), "foo") node.Role = "rolo" node.Description = "desc" node.Address = "addr" ds.addNode(node) - nodes := ds.GetNodes() + return ds, err +} + +func TestFileDatastoreInit(tester *testing.T) { + defer cleanup() + ds, err := createDatastore(true) + assert.NoError(tester, err) + assert.Equal(tester, DEFAULT_RETRY_FAILURE_INTERVAL_MS, ds.retryFailureIntervalMs) +} + +func TestNodes(tester *testing.T) { + defer cleanup() + ds, _ := createDatastore(true) + nodes := ds.GetNodes(newContext()) if assert.Len(tester, nodes, 1) { assert.Equal(tester, "foo", nodes[0].Id) assert.Equal(tester, "rolo", nodes[0].Role) @@ -64,20 +77,17 @@ func TestNodes(tester *testing.T) { assert.Equal(tester, "addr", nodes[0].Address) } - node = ds.CreateNode("bar") + node := ds.CreateNode(newContext(), "bar") ds.addNode(node) - nodes = ds.GetNodes() + nodes = ds.GetNodes(newContext()) assert.Len(tester, nodes, 2) job := ds.GetNextJob(newContext(), "foo") assert.Nil(tester, job) } func TestJobs(tester *testing.T) { - ds := NewFileDatastoreImpl(fake.NewAuthorizedServer(nil)) - cfg := make(module.ModuleConfig) - ds.Init(cfg) - node := ds.CreateNode("foo") - ds.addNode(node) + defer cleanup() + ds, _ := createDatastore(true) // Test adding a job job := ds.CreateJob(newContext()) @@ -110,12 +120,65 @@ func TestJobs(tester *testing.T) { assert.Len(tester, jobs, 0) } +func TestJobAddUnauthorized(tester *testing.T) { + defer cleanup() + ds, _ := createDatastore(false) + + // Test adding a job + job := ds.CreateJob(newContext()) + assert.Equal(tester, 1001, job.Id) + err := ds.AddJob(newContext(), job) + assert.Error(tester, err) + assert.Len(tester, ds.jobsById, 0) +} + +func TestJobAdd(tester *testing.T) { + defer cleanup() + ds, _ := createDatastore(true) + assert.Len(tester, ds.jobsById, 0) + + // Test adding a job + job := ds.CreateJob(newContext()) + assert.Equal(tester, 1001, job.Id) + err := ds.AddJob(newContext(), job) + assert.NoError(tester, err) + assert.Len(tester, ds.jobsById, 1) + + newJob := ds.GetJob(newContext(), job.Id) + assert.Equal(tester, MY_USER_ID, newJob.UserId) +} + +func TestJobAddPivotUnauthorized(tester *testing.T) { + defer cleanup() + ds, _ := createDatastore(false) + + // Test adding an arbitrary job + job := ds.CreateJob(newContext()) + assert.Equal(tester, 1001, job.Id) + err := ds.AddPivotJob(newContext(), job) + assert.Error(tester, err) + assert.Len(tester, ds.jobsById, 0) +} + +func TestJobAddPivot(tester *testing.T) { + defer cleanup() + ds, _ := createDatastore(true) + assert.Len(tester, ds.jobsById, 0) + + // Test adding a pivot job (requires different permission) + job := ds.CreateJob(newContext()) + assert.Equal(tester, 1001, job.Id) + err := ds.AddPivotJob(newContext(), job) + assert.NoError(tester, err) + assert.Len(tester, ds.jobsById, 1) + + newJob := ds.GetJob(newContext(), job.Id) + assert.Equal(tester, MY_USER_ID, newJob.UserId) +} + func TestJobReadAuthorization(tester *testing.T) { - ds := NewFileDatastoreImpl(fake.NewUnauthorizedServer()) - cfg := make(module.ModuleConfig) - ds.Init(cfg) - node := ds.CreateNode("foo") - ds.addNode(node) + defer cleanup() + ds, _ := createDatastore(false) myJobId := 10001 anotherJobId := 10002 @@ -143,11 +206,8 @@ func TestJobReadAuthorization(tester *testing.T) { } func TestJobDeleteAuthorization(tester *testing.T) { - ds := NewFileDatastoreImpl(fake.NewUnauthorizedServer()) - cfg := make(module.ModuleConfig) - ds.Init(cfg) - node := ds.CreateNode("foo") - ds.addNode(node) + defer cleanup() + ds, _ := createDatastore(false) myJobId := 10001 anotherJobId := 10002 @@ -176,21 +236,18 @@ func TestJobDeleteAuthorization(tester *testing.T) { } func TestGetStreamFilename(tester *testing.T) { - ds := NewFileDatastoreImpl(fake.NewUnauthorizedServer()) - cfg := make(module.ModuleConfig) - cfg["jobDir"] = "/tmp/jobs" - ds.Init(cfg) + defer cleanup() + ds, _ := createDatastore(false) filename := ds.getStreamFilename(ds.CreateJob(newContext())) - assert.Equal(tester, "/tmp/jobs/1001.bin", filename) + assert.Equal(tester, "/tmp/sensoroni.jobs/1001.bin", filename) } func TestUpdateInelegible(tester *testing.T) { - ds := NewFileDatastoreImpl(fake.NewUnauthorizedServer()) - cfg := make(module.ModuleConfig) - ds.Init(cfg) + defer cleanup() + ds, _ := createDatastore(false) job := ds.CreateJob(newContext()) - job.UserId = MY_USER_ID // This user's job + job.UserId = MY_USER_ID job.Id = 1212 ds.addJob(job) @@ -199,12 +256,11 @@ func TestUpdateInelegible(tester *testing.T) { } func TestUpdatePreserveData(tester *testing.T) { - ds := NewFileDatastoreImpl(fake.NewAuthorizedServer(nil)) - cfg := make(module.ModuleConfig) - ds.Init(cfg) + defer cleanup() + ds, _ := createDatastore(true) job := ds.CreateJob(newContext()) - job.UserId = MY_USER_ID // This user's job + job.UserId = MY_USER_ID job.NodeId = "some node" job.Id = 1212 job.Status = model.JobStatusPending diff --git a/server/modules/influxdb/influxdb.go b/server/modules/influxdb/influxdb.go index 6d051f4f..a4666c27 100644 --- a/server/modules/influxdb/influxdb.go +++ b/server/modules/influxdb/influxdb.go @@ -14,21 +14,21 @@ import ( "github.com/security-onion-solutions/securityonion-soc/server" ) -const DEFAULT_ORG = "" -const DEFAULT_BUCKET = "telegraf" -const DEFAULT_CACHE_EXPIRATION_MS = 60000 -const DEFAULT_MAX_METRIC_AGE_SECONDS = 1200 +const DEFAULT_ORG = "" +const DEFAULT_BUCKET = "telegraf" +const DEFAULT_CACHE_EXPIRATION_MS = 60000 +const DEFAULT_MAX_METRIC_AGE_SECONDS = 1200 type InfluxDB struct { - config module.ModuleConfig - server *server.Server - metrics *InfluxDBMetrics + config module.ModuleConfig + server *server.Server + metrics *InfluxDBMetrics } func NewInfluxDB(srv *server.Server) *InfluxDB { - return &InfluxDB { - server: srv, - metrics: NewInfluxDBMetrics(), + return &InfluxDB{ + server: srv, + metrics: NewInfluxDBMetrics(srv), } } @@ -40,11 +40,11 @@ func (influxdb *InfluxDB) Init(cfg module.ModuleConfig) error { influxdb.config = cfg host, _ := module.GetString(cfg, "hostUrl") verifyCert := module.GetBoolDefault(cfg, "verifyCert", true) - token, _ := module.GetString(cfg, "token") - org := module.GetStringDefault(cfg, "org", DEFAULT_ORG) - bucket := module.GetStringDefault(cfg, "bucket", DEFAULT_BUCKET) - cacheExpirationMs := module.GetIntDefault(cfg, "cacheExpirationMs", DEFAULT_CACHE_EXPIRATION_MS) - maxMetricAgeSeconds := module.GetIntDefault(cfg, "maxMetricAgeSeconds", DEFAULT_MAX_METRIC_AGE_SECONDS) + token, _ := module.GetString(cfg, "token") + org := module.GetStringDefault(cfg, "org", DEFAULT_ORG) + bucket := module.GetStringDefault(cfg, "bucket", DEFAULT_BUCKET) + cacheExpirationMs := module.GetIntDefault(cfg, "cacheExpirationMs", DEFAULT_CACHE_EXPIRATION_MS) + maxMetricAgeSeconds := module.GetIntDefault(cfg, "maxMetricAgeSeconds", DEFAULT_MAX_METRIC_AGE_SECONDS) err := influxdb.metrics.Init(host, token, org, bucket, verifyCert, cacheExpirationMs, maxMetricAgeSeconds) if err == nil && influxdb.server != nil { influxdb.server.Metrics = influxdb.metrics @@ -57,10 +57,10 @@ func (influxdb *InfluxDB) Start() error { } func (influxdb *InfluxDB) Stop() error { - influxdb.metrics.Stop() + influxdb.metrics.Stop() return nil } func (influxdb *InfluxDB) IsRunning() bool { return influxdb.metrics.client != nil -} \ No newline at end of file +} diff --git a/server/modules/influxdb/influxdbmetrics.go b/server/modules/influxdb/influxdbmetrics.go index fdc47f9d..81169685 100644 --- a/server/modules/influxdb/influxdbmetrics.go +++ b/server/modules/influxdb/influxdbmetrics.go @@ -16,6 +16,7 @@ import ( "github.com/influxdata/influxdb-client-go/v2" "github.com/influxdata/influxdb-client-go/v2/api" "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/server" "strconv" "sync" "time" @@ -23,6 +24,7 @@ import ( type InfluxDBMetrics struct { client influxdb2.Client + server *server.Server token string org string bucket string @@ -41,8 +43,9 @@ type InfluxDBMetrics struct { failedEvents map[string]int } -func NewInfluxDBMetrics() *InfluxDBMetrics { +func NewInfluxDBMetrics(srv *server.Server) *InfluxDBMetrics { return &InfluxDBMetrics{ + server: srv, raidStatus: make(map[string]int), processStatus: make(map[string]int), consumptionEps: make(map[string]int), @@ -248,24 +251,29 @@ func (metrics *InfluxDBMetrics) getFailedEvents(host string) int { return metrics.failedEvents[host] } -func (metrics *InfluxDBMetrics) GetGridEps() int { - metrics.updateEps() - +func (metrics *InfluxDBMetrics) GetGridEps(ctx context.Context) int { eps := 0 - for _, hostEps := range metrics.consumptionEps { - eps = eps + hostEps + if err := metrics.server.Authorizer.CheckContextOperationAuthorized(ctx, "read", "nodes"); err == nil { + metrics.updateEps() + for _, hostEps := range metrics.consumptionEps { + eps = eps + hostEps + } } return eps } -func (metrics *InfluxDBMetrics) UpdateNodeMetrics(node *model.Node) bool { - node.RaidStatus = metrics.getRaidStatus(node.Id) - node.ProcessStatus = metrics.getProcessStatus(node.Id) - node.ProductionEps = metrics.getProductionEps(node.Id) - node.ConsumptionEps = metrics.getConsumptionEps(node.Id) - node.FailedEvents = metrics.getFailedEvents(node.Id) - - enhancedStatusEnabled := (metrics.client != nil) - return node.UpdateOverallStatus(enhancedStatusEnabled) +func (metrics *InfluxDBMetrics) UpdateNodeMetrics(ctx context.Context, node *model.Node) bool { + var status bool + if err := metrics.server.Authorizer.CheckContextOperationAuthorized(ctx, "write", "nodes"); err == nil { + node.RaidStatus = metrics.getRaidStatus(node.Id) + node.ProcessStatus = metrics.getProcessStatus(node.Id) + node.ProductionEps = metrics.getProductionEps(node.Id) + node.ConsumptionEps = metrics.getConsumptionEps(node.Id) + node.FailedEvents = metrics.getFailedEvents(node.Id) + + enhancedStatusEnabled := (metrics.client != nil) + status = node.UpdateOverallStatus(enhancedStatusEnabled) + } + return status } diff --git a/server/modules/influxdb/influxdbmetrics_test.go b/server/modules/influxdb/influxdbmetrics_test.go index b5b3553e..a66ee092 100644 --- a/server/modules/influxdb/influxdbmetrics_test.go +++ b/server/modules/influxdb/influxdbmetrics_test.go @@ -10,15 +10,21 @@ package influxdb import ( + "context" "testing" "time" + "github.com/security-onion-solutions/securityonion-soc/fake" "github.com/security-onion-solutions/securityonion-soc/model" "github.com/stretchr/testify/assert" ) +func newContext() context.Context { + return context.Background() +} + func TestConvertValuesToString(tester *testing.T) { - metrics := NewInfluxDBMetrics() + metrics := NewInfluxDBMetrics(fake.NewAuthorizedServer(nil)) values := make(map[string]interface{}) values["foo"] = "bar" values["bar"] = 1 @@ -27,7 +33,7 @@ func TestConvertValuesToString(tester *testing.T) { } func TestConvertValuesToInt(tester *testing.T) { - metrics := NewInfluxDBMetrics() + metrics := NewInfluxDBMetrics(fake.NewAuthorizedServer(nil)) values := make(map[string]interface{}) values["foo"] = 1234 values["bar"] = 9876.1 @@ -39,7 +45,7 @@ func TestConvertValuesToInt(tester *testing.T) { } func TestGetRaidStatus(tester *testing.T) { - metrics := NewInfluxDBMetrics() + metrics := NewInfluxDBMetrics(fake.NewAuthorizedServer(nil)) metrics.lastRaidUpdateTime = time.Now() metrics.raidStatus["foo"] = 0 metrics.raidStatus["bar"] = 1 @@ -50,7 +56,7 @@ func TestGetRaidStatus(tester *testing.T) { } func TestGetProcessStatus(tester *testing.T) { - metrics := NewInfluxDBMetrics() + metrics := NewInfluxDBMetrics(fake.NewAuthorizedServer(nil)) metrics.lastProcessUpdateTime = time.Now() metrics.processStatus["foo"] = 0 metrics.processStatus["bar"] = 1 @@ -61,7 +67,7 @@ func TestGetProcessStatus(tester *testing.T) { } func TestGetProductionEps(tester *testing.T) { - metrics := NewInfluxDBMetrics() + metrics := NewInfluxDBMetrics(fake.NewAuthorizedServer(nil)) metrics.lastEpsUpdateTime = time.Now() metrics.productionEps["foo"] = 0 metrics.productionEps["bar"] = 1 @@ -74,7 +80,7 @@ func TestGetProductionEps(tester *testing.T) { } func TestGetConsumptionEps(tester *testing.T) { - metrics := NewInfluxDBMetrics() + metrics := NewInfluxDBMetrics(fake.NewAuthorizedServer(nil)) metrics.lastEpsUpdateTime = time.Now() metrics.consumptionEps["foo"] = 0 metrics.consumptionEps["bar"] = 1 @@ -84,11 +90,11 @@ func TestGetConsumptionEps(tester *testing.T) { assert.Equal(tester, 1, metrics.getConsumptionEps("bar")) assert.Equal(tester, 2, metrics.getConsumptionEps("zoo")) assert.Equal(tester, 0, metrics.getConsumptionEps("missing")) - assert.Equal(tester, 3, metrics.GetGridEps()) + assert.Equal(tester, 3, metrics.GetGridEps(newContext())) } func TestGetFailedEvents(tester *testing.T) { - metrics := NewInfluxDBMetrics() + metrics := NewInfluxDBMetrics(fake.NewAuthorizedServer(nil)) metrics.lastEpsUpdateTime = time.Now() metrics.failedEvents["foo"] = 0 metrics.failedEvents["bar"] = 1 diff --git a/server/modules/kratos/kratosuserstore.go b/server/modules/kratos/kratosuserstore.go index 4af52204..5331f2c0 100644 --- a/server/modules/kratos/kratosuserstore.go +++ b/server/modules/kratos/kratosuserstore.go @@ -84,11 +84,14 @@ func (kratos *KratosUserstore) GetUsers(ctx context.Context) ([]*model.User, err return users, nil } -func (kratos *KratosUserstore) DeleteUser(id string) error { - log.WithField("id", id).Debug("Deleting user") - _, err := kratos.client.SendObject("DELETE", "/identities/"+id, "", nil, false) - if err != nil { - log.WithError(err).Error("Failed to delete user from Kratos") +func (kratos *KratosUserstore) DeleteUser(ctx context.Context, id string) error { + var err error + if err = kratos.server.Authorizer.CheckContextOperationAuthorized(ctx, "delete", "users"); err != nil { + log.WithField("id", id).Debug("Deleting user") + _, err := kratos.client.SendObject("DELETE", "/identities/"+id, "", nil, false) + if err != nil { + log.WithError(err).Error("Failed to delete user from Kratos") + } } return err } @@ -109,15 +112,18 @@ func (kratos *KratosUserstore) GetUser(ctx context.Context, id string) (*model.U return user, err } -func (kratos *KratosUserstore) UpdateUser(id string, user *model.User) error { - kratosUser, err := kratos.fetchUser(id) - if err != nil { - log.WithError(err).Error("Original user not found") - } else { - kratosUser.copyFromUser(user) - _, err = kratos.client.SendObject("PUT", "/identities/"+id, kratosUser, nil, false) +func (kratos *KratosUserstore) UpdateUser(ctx context.Context, id string, user *model.User) error { + var err error + if err = kratos.server.Authorizer.CheckContextOperationAuthorized(ctx, "write", "users"); err != nil { + kratosUser, err := kratos.fetchUser(id) if err != nil { - log.WithError(err).Error("Failed to update user in Kratos") + log.WithError(err).Error("Original user not found") + } else { + kratosUser.copyFromUser(user) + _, err = kratos.client.SendObject("PUT", "/identities/"+id, kratosUser, nil, false) + if err != nil { + log.WithError(err).Error("Failed to update user in Kratos") + } } } return err diff --git a/server/modules/sostatus/sostatus.go b/server/modules/sostatus/sostatus.go index 8a73797a..e45a8d08 100644 --- a/server/modules/sostatus/sostatus.go +++ b/server/modules/sostatus/sostatus.go @@ -10,49 +10,56 @@ package sostatus import ( - "time" + "context" "github.com/apex/log" - "github.com/security-onion-solutions/securityonion-soc/model" - "github.com/security-onion-solutions/securityonion-soc/module" - "github.com/security-onion-solutions/securityonion-soc/server" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/module" + "github.com/security-onion-solutions/securityonion-soc/server" + "time" ) -const DEFAULT_REFRESH_INTERVAL_MS = 30000 -const DEFAULT_OFFLINE_THRESHOLD_MS = 60000 +const DEFAULT_REFRESH_INTERVAL_MS = 30000 +const DEFAULT_OFFLINE_THRESHOLD_MS = 60000 type SoStatus struct { - config module.ModuleConfig - server *server.Server - stopChannel chan int - refreshTicker *time.Ticker - running bool - refreshIntervalMs int - offlineThresholdMs int - currentStatus *model.Status + config module.ModuleConfig + server *server.Server + stopChannel chan int + refreshTicker *time.Ticker + running bool + refreshIntervalMs int + offlineThresholdMs int + currentStatus *model.Status + ctx context.Context } func NewSoStatus(srv *server.Server) *SoStatus { - return &SoStatus { - server: srv, - } + return &SoStatus{ + server: srv, + } } func (status *SoStatus) PrerequisiteModules() []string { - return nil + return nil +} + +func (status *SoStatus) newServerContext() context.Context { + return status.server.Context } func (status *SoStatus) Init(cfg module.ModuleConfig) error { - status.config = cfg - status.refreshIntervalMs = module.GetIntDefault(cfg, "refreshIntervalMs", DEFAULT_REFRESH_INTERVAL_MS) - status.offlineThresholdMs = module.GetIntDefault(cfg, "offlineThresholdMs", DEFAULT_OFFLINE_THRESHOLD_MS) - status.currentStatus = model.NewStatus() - return nil + status.config = cfg + status.refreshIntervalMs = module.GetIntDefault(cfg, "refreshIntervalMs", DEFAULT_REFRESH_INTERVAL_MS) + status.offlineThresholdMs = module.GetIntDefault(cfg, "offlineThresholdMs", DEFAULT_OFFLINE_THRESHOLD_MS) + status.currentStatus = model.NewStatus() + status.ctx = status.newServerContext() + return nil } func (status *SoStatus) Start() error { status.stopChannel = make(chan int) go status.refresher() - return nil + return nil } func (status *SoStatus) refresher() { @@ -61,9 +68,9 @@ func (status *SoStatus) refresher() { for { select { - case <- status.refreshTicker.C: - status.Refresh() - case <- status.stopChannel: + case <-status.refreshTicker.C: + status.Refresh(status.ctx) + case <-status.stopChannel: status.refreshTicker.Stop() return } @@ -73,57 +80,57 @@ func (status *SoStatus) refresher() { func (status *SoStatus) Stop() error { close(status.stopChannel) - return nil + return nil } func (status *SoStatus) IsRunning() bool { - return status.running + return status.running } -func (status *SoStatus) Refresh() { +func (status *SoStatus) Refresh(ctx context.Context) { log.Debug("Updating grid status") - status.refreshGrid() + status.refreshGrid(ctx) status.server.Host.Broadcast("status", status.currentStatus) } -func (status *SoStatus) refreshGrid() { +func (status *SoStatus) refreshGrid(ctx context.Context) { unhealthyNodes := 0 - nodes := status.server.Datastore.GetNodes() + nodes := status.server.Datastore.GetNodes(ctx) for _, node := range nodes { staleMs := int(time.Now().Sub(node.UpdateTime) / time.Millisecond) if staleMs > status.offlineThresholdMs { if node.ConnectionStatus != model.NodeStatusFault { - log.WithFields(log.Fields { - "nodeId": node.Id, - "staleMs": staleMs, + log.WithFields(log.Fields{ + "nodeId": node.Id, + "staleMs": staleMs, "offlineThresholdMs": status.offlineThresholdMs, }).Warn("Node has gone offline") node.ConnectionStatus = model.NodeStatusFault } } - updated := status.server.Metrics.UpdateNodeMetrics(node) + updated := status.server.Metrics.UpdateNodeMetrics(ctx, node) - log.WithFields(log.Fields { - "Id": node.Id, - "processStatus": node.ProcessStatus, - "raidStatus": node.RaidStatus, - "connectionStatus": node.ConnectionStatus, - "overallStatus": node.Status, - "updated": updated, - }).Debug("Node Status") + log.WithFields(log.Fields{ + "Id": node.Id, + "processStatus": node.ProcessStatus, + "raidStatus": node.RaidStatus, + "connectionStatus": node.ConnectionStatus, + "overallStatus": node.Status, + "updated": updated, + }).Debug("Node Status") - if updated { - status.server.Host.Broadcast("node", node) - } + if updated { + status.server.Host.Broadcast("node", node) + } - if node.Status != model.NodeStatusOk { + if node.Status != model.NodeStatusOk { unhealthyNodes++ - } + } } status.currentStatus.Grid.TotalNodeCount = len(nodes) status.currentStatus.Grid.UnhealthyNodeCount = unhealthyNodes - status.currentStatus.Grid.Eps = status.server.Metrics.GetGridEps() -} \ No newline at end of file + status.currentStatus.Grid.Eps = status.server.Metrics.GetGridEps(ctx) +} diff --git a/server/modules/sostatus/sostatus_test.go b/server/modules/sostatus/sostatus_test.go index f53677d7..fe9a4b5e 100644 --- a/server/modules/sostatus/sostatus_test.go +++ b/server/modules/sostatus/sostatus_test.go @@ -10,13 +10,13 @@ package sostatus import ( - "testing" - + "github.com/security-onion-solutions/securityonion-soc/fake" "github.com/stretchr/testify/assert" + "testing" ) func TestSoStatusInit(tester *testing.T) { - status := NewSoStatus(nil) + status := NewSoStatus(fake.NewUnauthorizedServer()) cfg := make(map[string]interface{}) cfg["refreshIntervalMs"] = float64(1000) cfg["offlineThresholdMs"] = float64(2000) diff --git a/server/modules/statickeyauth/statickeyauth.go b/server/modules/statickeyauth/statickeyauth.go index 5d1faa8d..24240c59 100644 --- a/server/modules/statickeyauth/statickeyauth.go +++ b/server/modules/statickeyauth/statickeyauth.go @@ -24,7 +24,7 @@ type StaticKeyAuth struct { func NewStaticKeyAuth(srv *server.Server) *StaticKeyAuth { return &StaticKeyAuth{ server: srv, - impl: NewStaticKeyAuthImpl(), + impl: NewStaticKeyAuthImpl(srv), } } diff --git a/server/modules/statickeyauth/statickeyauthimpl.go b/server/modules/statickeyauth/statickeyauthimpl.go index 7e106616..f0c6ca32 100644 --- a/server/modules/statickeyauth/statickeyauthimpl.go +++ b/server/modules/statickeyauth/statickeyauthimpl.go @@ -14,8 +14,7 @@ import ( "context" "errors" "github.com/apex/log" - "github.com/security-onion-solutions/securityonion-soc/model" - "github.com/security-onion-solutions/securityonion-soc/web" + "github.com/security-onion-solutions/securityonion-soc/server" "net" "net/http" "strings" @@ -24,10 +23,13 @@ import ( type StaticKeyAuthImpl struct { apiKey string anonymousNetwork *net.IPNet + server *server.Server } -func NewStaticKeyAuthImpl() *StaticKeyAuthImpl { - return &StaticKeyAuthImpl{} +func NewStaticKeyAuthImpl(srv *server.Server) *StaticKeyAuthImpl { + return &StaticKeyAuthImpl{ + server: srv, + } } func (auth *StaticKeyAuthImpl) Init(apiKey string, anonymousCidr string) error { @@ -49,11 +51,9 @@ func (auth *StaticKeyAuthImpl) Preprocess(ctx context.Context, req *http.Request statusCode = http.StatusUnauthorized err = errors.New("Access denied") } else { - // Currently all static auth clients are sensors - sensorUser := model.NewUser() - sensorUser.Id = "sensor" - sensorUser.Email = "sensor" - ctx = context.WithValue(ctx, web.ContextKeyRequestor, sensorUser) + // Remote agents will assume the role of this server until the implementation + // is enhanced to support unique agent keys and roles. + ctx = auth.server.Context } return ctx, statusCode, err } diff --git a/server/modules/statickeyauth/statickeyauthimpl_test.go b/server/modules/statickeyauth/statickeyauthimpl_test.go index 39baba2a..946e8b92 100644 --- a/server/modules/statickeyauth/statickeyauthimpl_test.go +++ b/server/modules/statickeyauth/statickeyauthimpl_test.go @@ -15,6 +15,7 @@ import ( "net/http" "testing" + "github.com/security-onion-solutions/securityonion-soc/fake" "github.com/security-onion-solutions/securityonion-soc/model" "github.com/security-onion-solutions/securityonion-soc/web" "github.com/stretchr/testify/assert" @@ -30,7 +31,7 @@ func TestValidateAuthorization(tester *testing.T) { } func validateAuthorization(tester *testing.T, key string, ip string, expected bool) { - ai := NewStaticKeyAuthImpl() + ai := NewStaticKeyAuthImpl(fake.NewAuthorizedServer(nil)) ai.Init("abc", "172.17.0.0/24") actual := ai.validateAuthorization(key, ip) assert.Equal(tester, expected, actual) @@ -45,14 +46,14 @@ func TestValidateApiKey(tester *testing.T) { } func validateKey(tester *testing.T, key string, expected bool) { - ai := NewStaticKeyAuthImpl() + ai := NewStaticKeyAuthImpl(fake.NewAuthorizedServer(nil)) ai.apiKey = "abc" actual := ai.validateApiKey(key) assert.Equal(tester, expected, actual) } func TestAuthImplInit(tester *testing.T) { - ai := NewStaticKeyAuthImpl() + ai := NewStaticKeyAuthImpl(fake.NewAuthorizedServer(nil)) err := ai.Init("abc", "1") assert.Error(tester, err) err = ai.Init("abc", "1.2.3.4/16") @@ -63,12 +64,12 @@ func TestAuthImplInit(tester *testing.T) { } func TestPreprocessPriority(tester *testing.T) { - handler := NewStaticKeyAuthImpl() + handler := NewStaticKeyAuthImpl(fake.NewAuthorizedServer(nil)) assert.Equal(tester, 100, handler.PreprocessPriority()) } func TestPreprocess(tester *testing.T) { - ai := NewStaticKeyAuthImpl() + ai := NewStaticKeyAuthImpl(fake.NewAuthorizedServer(nil)) err := ai.Init("abc", "1") assert.Error(tester, err) ai.apiKey = "123" @@ -82,8 +83,8 @@ func TestPreprocess(tester *testing.T) { if assert.NotNil(tester, requestor) { sensorUser := requestor.(*model.User) assert.NotNil(tester, sensorUser) - assert.Equal(tester, "sensor", sensorUser.Id) - assert.Equal(tester, "sensor", sensorUser.Email) + assert.Equal(tester, "agent", sensorUser.Id) + assert.Equal(tester, "agent", sensorUser.Email) } } } diff --git a/server/modules/thehive/thehive.go b/server/modules/thehive/thehive.go index 219e30b6..503a773c 100644 --- a/server/modules/thehive/thehive.go +++ b/server/modules/thehive/thehive.go @@ -15,15 +15,15 @@ import ( ) type TheHive struct { - config module.ModuleConfig - server *server.Server - store *TheHiveCasestore + config module.ModuleConfig + server *server.Server + store *TheHiveCasestore } func NewTheHive(srv *server.Server) *TheHive { - return &TheHive { + return &TheHive{ server: srv, - store: NewTheHiveCasestore(), + store: NewTheHiveCasestore(srv), } } @@ -53,4 +53,4 @@ func (thehive *TheHive) Stop() error { func (somodule *TheHive) IsRunning() bool { return false -} \ No newline at end of file +} diff --git a/server/modules/thehive/thehivecasestore.go b/server/modules/thehive/thehivecasestore.go index 4b72a0e0..2277b8bb 100644 --- a/server/modules/thehive/thehivecasestore.go +++ b/server/modules/thehive/thehivecasestore.go @@ -11,23 +11,28 @@ package thehive import ( - "net/http" + "context" "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/server" "github.com/security-onion-solutions/securityonion-soc/web" + "net/http" ) type TheHiveCasestore struct { - client *web.Client - apiKey string + client *web.Client + server *server.Server + apiKey string } -func NewTheHiveCasestore() *TheHiveCasestore { - return &TheHiveCasestore{} +func NewTheHiveCasestore(srv *server.Server) *TheHiveCasestore { + return &TheHiveCasestore{ + server: srv, + } } -func (store *TheHiveCasestore) Init(hostUrl string, - key string, - verifyCert bool) error { +func (store *TheHiveCasestore) Init(hostUrl string, + key string, + verifyCert bool) error { store.client = web.NewClient(hostUrl, verifyCert) store.client.Auth = store store.apiKey = key @@ -35,21 +40,26 @@ func (store *TheHiveCasestore) Init(hostUrl string, } func (store *TheHiveCasestore) Authorize(request *http.Request) error { - request.Header.Add("Authorization", "Bearer " + store.apiKey) + request.Header.Add("Authorization", "Bearer "+store.apiKey) return nil } -func (store *TheHiveCasestore) Create(socCase *model.Case) (*model.Case, error) { - var outputCase TheHiveCase - inputCase, err := convertToTheHiveCase(socCase) - if err != nil { - return nil, err - } - _, err = store.client.SendAuthorizedObject("POST", "/api/case", inputCase, &outputCase) - if err != nil { - return nil, err +func (store *TheHiveCasestore) Create(ctx context.Context, socCase *model.Case) (*model.Case, error) { + var newCase *model.Case + var err error + + if err = store.server.Authorizer.CheckContextOperationAuthorized(ctx, "write", "cases"); err == nil { + var outputCase TheHiveCase + var inputCase *TheHiveCase + inputCase, err = convertToTheHiveCase(socCase) + if err != nil { + return nil, err + } + _, err = store.client.SendAuthorizedObject("POST", "/api/case", inputCase, &outputCase) + if err != nil { + return nil, err + } + newCase, err = convertFromTheHiveCase(&outputCase) } - newCase, err := convertFromTheHiveCase(&outputCase) return newCase, err } - diff --git a/server/modules/thehive/thehivecasestore_test.go b/server/modules/thehive/thehivecasestore_test.go new file mode 100644 index 00000000..e2fb9a73 --- /dev/null +++ b/server/modules/thehive/thehivecasestore_test.go @@ -0,0 +1,45 @@ +// 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 thehive + +import ( + "context" + "github.com/security-onion-solutions/securityonion-soc/fake" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCreateUnauthorized(tester *testing.T) { + casestore := NewTheHiveCasestore(fake.NewUnauthorizedServer()) + casestore.Init("some/url", "somekey", true) + socCase := model.NewCase() + newCase, err := casestore.Create(context.Background(), socCase) + assert.Error(tester, err) + assert.Nil(tester, newCase) +} + +func TestCreate(tester *testing.T) { + casestore := NewTheHiveCasestore(fake.NewAuthorizedServer(nil)) + casestore.Init("some/url", "somekey", true) + caseResponse := ` + { + "caseId": 123, + "title": "my title" + }` + casestore.client.MockStringResponse(caseResponse, 200, nil) + socCase := model.NewCase() + newCase, err := casestore.Create(context.Background(), socCase) + assert.NoError(tester, err) + + assert.Equal(tester, "my title", newCase.Title) + assert.Equal(tester, "123", newCase.Id) +} diff --git a/server/nodehandler.go b/server/nodehandler.go index fb37893c..fd8ed8dd 100644 --- a/server/nodehandler.go +++ b/server/nodehandler.go @@ -44,9 +44,9 @@ func (nodeHandler *NodeHandler) post(ctx context.Context, writer http.ResponseWr node := model.NewNode("") err := nodeHandler.ReadJson(request, node) if err == nil { - node, err = nodeHandler.server.Datastore.UpdateNode(node) + node, err = nodeHandler.server.Datastore.UpdateNode(ctx, node) if err == nil { - nodeHandler.server.Metrics.UpdateNodeMetrics(node) + nodeHandler.server.Metrics.UpdateNodeMetrics(ctx, node) nodeHandler.Host.Broadcast("node", node) job = nodeHandler.server.Datastore.GetNextJob(ctx, node.Id) } diff --git a/server/server.go b/server/server.go index d34b0cff..94e0d284 100644 --- a/server/server.go +++ b/server/server.go @@ -14,6 +14,7 @@ import ( "context" "github.com/apex/log" "github.com/security-onion-solutions/securityonion-soc/config" + "github.com/security-onion-solutions/securityonion-soc/model" "github.com/security-onion-solutions/securityonion-soc/web" "os/exec" "strings" @@ -34,14 +35,25 @@ type Server struct { Metrics Metrics stoppedChan chan bool Authorizer Authorizer + Context context.Context } func NewServer(cfg *config.ServerConfig, version string) *Server { - return &Server{ + server := &Server{ Config: cfg, Host: web.NewHost(cfg.BindAddress, cfg.HtmlDir, cfg.IdleConnectionTimeoutMs, version), stoppedChan: make(chan bool, 1), } + server.initContext() + return server +} + +func (server *Server) initContext() { + // Server will retain the role of an agent until there's a need for higher privileges + agent := model.NewUser() + agent.Id = "agent" + agent.Email = agent.Id + server.Context = context.WithValue(context.Background(), web.ContextKeyRequestor, agent) } func (server *Server) Start() { diff --git a/server/userhandler.go b/server/userhandler.go index 3aca5755..d96b4ce4 100644 --- a/server/userhandler.go +++ b/server/userhandler.go @@ -12,19 +12,19 @@ package server import ( "context" "errors" + "github.com/security-onion-solutions/securityonion-soc/model" + "github.com/security-onion-solutions/securityonion-soc/web" "net/http" - "regexp" - "github.com/security-onion-solutions/securityonion-soc/model" - "github.com/security-onion-solutions/securityonion-soc/web" + "regexp" ) type UserHandler struct { web.BaseHandler - server *Server + server *Server } func NewUserHandler(srv *Server) *UserHandler { - handler := &UserHandler {} + handler := &UserHandler{} handler.Host = srv.Host handler.server = srv handler.Impl = handler @@ -32,12 +32,13 @@ func NewUserHandler(srv *Server) *UserHandler { } 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") - } + if userHandler.server.Userstore == nil { + return http.StatusMethodNotAllowed, nil, errors.New("Users module not enabled") + } switch request.Method { - case http.MethodGet: return userHandler.get(ctx, writer, request) + case http.MethodGet: + return userHandler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } @@ -47,8 +48,8 @@ func (userHandler *UserHandler) get(ctx context.Context, writer http.ResponseWri safe, _ := regexp.MatchString(`^[A-Za-z0-9-]+$`, id) if !safe { return http.StatusBadRequest, nil, errors.New("Invalid id") - } - user, err := userHandler.server.Userstore.GetUser(ctx, id) + } + user, err := userHandler.server.Userstore.GetUser(ctx, id) if err != nil { return http.StatusBadRequest, nil, err } @@ -60,15 +61,15 @@ func (userHandler *UserHandler) put(ctx context.Context, writer http.ResponseWri safe, _ := regexp.MatchString(`^[A-Za-z0-9-]+$`, id) if !safe { return http.StatusBadRequest, nil, errors.New("Invalid id") - } + } user := model.NewUser() - err := userHandler.ReadJson(request, user) - if err != nil { - return http.StatusBadRequest, nil, errors.New("Invalid user object") - } - err = userHandler.server.Userstore.UpdateUser(id, user) - if err != nil { - return http.StatusBadRequest, nil, err - } + err := userHandler.ReadJson(request, user) + if err != nil { + return http.StatusBadRequest, nil, errors.New("Invalid user object") + } + err = userHandler.server.Userstore.UpdateUser(ctx, id, user) + if err != nil { + return http.StatusBadRequest, nil, err + } return http.StatusOK, nil, nil -} \ No newline at end of file +} diff --git a/server/usershandler.go b/server/usershandler.go index 91a9a433..ac8b32c4 100644 --- a/server/usershandler.go +++ b/server/usershandler.go @@ -12,18 +12,18 @@ package server import ( "context" "errors" + "github.com/security-onion-solutions/securityonion-soc/web" "net/http" "regexp" - "github.com/security-onion-solutions/securityonion-soc/web" ) type UsersHandler struct { web.BaseHandler - server *Server + server *Server } func NewUsersHandler(srv *Server) *UsersHandler { - handler := &UsersHandler {} + handler := &UsersHandler{} handler.Host = srv.Host handler.server = srv handler.Impl = handler @@ -31,12 +31,13 @@ func NewUsersHandler(srv *Server) *UsersHandler { } 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") - } + if usersHandler.server.Userstore == nil { + return http.StatusMethodNotAllowed, nil, errors.New("Users module not enabled") + } switch request.Method { - case http.MethodGet: return usersHandler.get(ctx, writer, request) + case http.MethodGet: + return usersHandler.get(ctx, writer, request) } return http.StatusMethodNotAllowed, nil, errors.New("Method not supported") } @@ -55,9 +56,9 @@ func (usersHandler *UsersHandler) delete(ctx context.Context, writer http.Respon if !safe { return http.StatusBadRequest, nil, errors.New("Invalid id") } - err := usersHandler.server.Userstore.DeleteUser(id) + err := usersHandler.server.Userstore.DeleteUser(ctx, id) if err != nil { return http.StatusBadRequest, nil, err } return http.StatusOK, nil, nil -} \ No newline at end of file +} diff --git a/server/userstore.go b/server/userstore.go index e4fa3c17..75f98e4b 100644 --- a/server/userstore.go +++ b/server/userstore.go @@ -17,7 +17,7 @@ import ( type Userstore interface { GetUsers(ctx context.Context) ([]*model.User, error) - DeleteUser(id string) error + DeleteUser(ctx context.Context, id string) error GetUser(ctx context.Context, id string) (*model.User, error) - UpdateUser(id string, user *model.User) error + UpdateUser(ctx context.Context, id string, user *model.User) error } From 4bfad1b51d8da7e9d0a152a47d394e27d80dfb90 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Thu, 16 Sep 2021 08:53:22 -0400 Subject: [PATCH 17/32] Refactor agent context on static key auth --- server/modules/statickeyauth/statickeyauthimpl.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/server/modules/statickeyauth/statickeyauthimpl.go b/server/modules/statickeyauth/statickeyauthimpl.go index f0c6ca32..f5194ad0 100644 --- a/server/modules/statickeyauth/statickeyauthimpl.go +++ b/server/modules/statickeyauth/statickeyauthimpl.go @@ -14,7 +14,9 @@ import ( "context" "errors" "github.com/apex/log" + "github.com/security-onion-solutions/securityonion-soc/model" "github.com/security-onion-solutions/securityonion-soc/server" + "github.com/security-onion-solutions/securityonion-soc/web" "net" "net/http" "strings" @@ -53,7 +55,13 @@ func (auth *StaticKeyAuthImpl) Preprocess(ctx context.Context, req *http.Request } else { // Remote agents will assume the role of this server until the implementation // is enhanced to support unique agent keys and roles. - ctx = auth.server.Context + if agent, ok := auth.server.Context.Value(web.ContextKeyRequestor).(*model.User); ok { + ctx = context.WithValue(ctx, web.ContextKeyRequestor, agent) + ctx = context.WithValue(ctx, web.ContextKeyRequestorId, agent.Id) + } else { + // Server wasn't initialized correctly, or something has corrupted the static server context. + err = errors.New("Agent not found in server context") + } } return ctx, statusCode, err } From 965adfc86f0dc8a17a3d244d4fd6ad4d48b70321 Mon Sep 17 00:00:00 2001 From: William Wernert Date: Thu, 16 Sep 2021 09:50:29 -0400 Subject: [PATCH 18/32] Bump JS dependencies * Rename moment-timezone-data to moment-timezone-with-data to match naming scheme from dev * Change version string for vue-chartjs to match actual version --- html/index.html | 14 +++++++------- html/js/external/axios-0.21.1.min.js | 2 -- html/js/external/axios-0.21.4.min.js | 3 +++ html/js/external/marked-2.0.1.min.js | 6 ------ html/js/external/marked-2.1.3.min.js | 6 ++++++ ....js => moment-timezone-with-data-0.5.33.min.js} | 0 html/js/external/vue-2.6.11.min.js | 6 ------ html/js/external/vue-2.6.14.min.js | 6 ++++++ ...artjs-2.7.1.min.js => vue-chartjs-3.5.1.min.js} | 3 ++- html/js/external/vue-router-3.5.1.min.js | 6 ------ html/js/external/vue-router-3.5.2.min.js | 11 +++++++++++ html/js/external/vuetify-v2.4.7.min.js | 6 ------ html/js/external/vuetify-v2.5.8.min.js | 6 ++++++ 13 files changed, 41 insertions(+), 34 deletions(-) delete mode 100644 html/js/external/axios-0.21.1.min.js create mode 100644 html/js/external/axios-0.21.4.min.js delete mode 100644 html/js/external/marked-2.0.1.min.js create mode 100644 html/js/external/marked-2.1.3.min.js rename html/js/external/{moment-timezone-data-0.5.33.min.js => moment-timezone-with-data-0.5.33.min.js} (100%) delete mode 100644 html/js/external/vue-2.6.11.min.js create mode 100644 html/js/external/vue-2.6.14.min.js rename html/js/external/{vue-chartjs-2.7.1.min.js => vue-chartjs-3.5.1.min.js} (56%) delete mode 100644 html/js/external/vue-router-3.5.1.min.js create mode 100644 html/js/external/vue-router-3.5.2.min.js delete mode 100644 html/js/external/vuetify-v2.4.7.min.js create mode 100644 html/js/external/vuetify-v2.5.8.min.js diff --git a/html/index.html b/html/index.html index e28230dc..95f874c6 100644 --- a/html/index.html +++ b/html/index.html @@ -1267,16 +1267,16 @@

- - - - + + + + - + - + - + diff --git a/html/js/external/axios-0.21.1.min.js b/html/js/external/axios-0.21.1.min.js deleted file mode 100644 index 394c74ca..00000000 --- a/html/js/external/axios-0.21.1.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/* axios v0.21.1 | (c) 2020 by Matt Zabriskie */ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";function r(e){var t=new i(e),n=s(i.prototype.request,t);return o.extend(n,i.prototype,t),o.extend(n,t),n}var o=n(2),s=n(3),i=n(4),a=n(22),u=n(10),c=r(u);c.Axios=i,c.create=function(e){return r(a(c.defaults,e))},c.Cancel=n(23),c.CancelToken=n(24),c.isCancel=n(9),c.all=function(e){return Promise.all(e)},c.spread=n(25),c.isAxiosError=n(26),e.exports=c,e.exports.default=c},function(e,t,n){"use strict";function r(e){return"[object Array]"===R.call(e)}function o(e){return"undefined"==typeof e}function s(e){return null!==e&&!o(e)&&null!==e.constructor&&!o(e.constructor)&&"function"==typeof e.constructor.isBuffer&&e.constructor.isBuffer(e)}function i(e){return"[object ArrayBuffer]"===R.call(e)}function a(e){return"undefined"!=typeof FormData&&e instanceof FormData}function u(e){var t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer}function c(e){return"string"==typeof e}function f(e){return"number"==typeof e}function p(e){return null!==e&&"object"==typeof e}function d(e){if("[object Object]"!==R.call(e))return!1;var t=Object.getPrototypeOf(e);return null===t||t===Object.prototype}function l(e){return"[object Date]"===R.call(e)}function h(e){return"[object File]"===R.call(e)}function m(e){return"[object Blob]"===R.call(e)}function y(e){return"[object Function]"===R.call(e)}function g(e){return p(e)&&y(e.pipe)}function v(e){return"undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams}function x(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function w(){return("undefined"==typeof navigator||"ReactNative"!==navigator.product&&"NativeScript"!==navigator.product&&"NS"!==navigator.product)&&("undefined"!=typeof window&&"undefined"!=typeof document)}function b(e,t){if(null!==e&&"undefined"!=typeof e)if("object"!=typeof e&&(e=[e]),r(e))for(var n=0,o=e.length;n=200&&e<300}};u.headers={common:{Accept:"application/json, text/plain, */*"}},s.forEach(["delete","get","head"],function(e){u.headers[e]={}}),s.forEach(["post","put","patch"],function(e){u.headers[e]=s.merge(a)}),e.exports=u},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){r.forEach(e,function(n,r){r!==t&&r.toUpperCase()===t.toUpperCase()&&(e[t]=n,delete e[r])})}},function(e,t,n){"use strict";var r=n(2),o=n(13),s=n(16),i=n(5),a=n(17),u=n(20),c=n(21),f=n(14);e.exports=function(e){return new Promise(function(t,n){var p=e.data,d=e.headers;r.isFormData(p)&&delete d["Content-Type"];var l=new XMLHttpRequest;if(e.auth){var h=e.auth.username||"",m=e.auth.password?unescape(encodeURIComponent(e.auth.password)):"";d.Authorization="Basic "+btoa(h+":"+m)}var y=a(e.baseURL,e.url);if(l.open(e.method.toUpperCase(),i(y,e.params,e.paramsSerializer),!0),l.timeout=e.timeout,l.onreadystatechange=function(){if(l&&4===l.readyState&&(0!==l.status||l.responseURL&&0===l.responseURL.indexOf("file:"))){var r="getAllResponseHeaders"in l?u(l.getAllResponseHeaders()):null,s=e.responseType&&"text"!==e.responseType?l.response:l.responseText,i={data:s,status:l.status,statusText:l.statusText,headers:r,config:e,request:l};o(t,n,i),l=null}},l.onabort=function(){l&&(n(f("Request aborted",e,"ECONNABORTED",l)),l=null)},l.onerror=function(){n(f("Network Error",e,null,l)),l=null},l.ontimeout=function(){var t="timeout of "+e.timeout+"ms exceeded";e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),n(f(t,e,"ECONNABORTED",l)),l=null},r.isStandardBrowserEnv()){var g=(e.withCredentials||c(y))&&e.xsrfCookieName?s.read(e.xsrfCookieName):void 0;g&&(d[e.xsrfHeaderName]=g)}if("setRequestHeader"in l&&r.forEach(d,function(e,t){"undefined"==typeof p&&"content-type"===t.toLowerCase()?delete d[t]:l.setRequestHeader(t,e)}),r.isUndefined(e.withCredentials)||(l.withCredentials=!!e.withCredentials),e.responseType)try{l.responseType=e.responseType}catch(t){if("json"!==e.responseType)throw t}"function"==typeof e.onDownloadProgress&&l.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&l.upload&&l.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then(function(e){l&&(l.abort(),n(e),l=null)}),p||(p=null),l.send(p)})}},function(e,t,n){"use strict";var r=n(14);e.exports=function(e,t,n){var o=n.config.validateStatus;n.status&&o&&!o(n.status)?t(r("Request failed with status code "+n.status,n.config,null,n.request,n)):e(n)}},function(e,t,n){"use strict";var r=n(15);e.exports=function(e,t,n,o,s){var i=new Error(e);return r(i,t,n,o,s)}},function(e,t){"use strict";e.exports=function(e,t,n,r,o){return e.config=t,n&&(e.code=n),e.request=r,e.response=o,e.isAxiosError=!0,e.toJSON=function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code}},e}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){return{write:function(e,t,n,o,s,i){var a=[];a.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&a.push("expires="+new Date(n).toGMTString()),r.isString(o)&&a.push("path="+o),r.isString(s)&&a.push("domain="+s),i===!0&&a.push("secure"),document.cookie=a.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},function(e,t,n){"use strict";var r=n(18),o=n(19);e.exports=function(e,t){return e&&!r(t)?o(e,t):t}},function(e,t){"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},function(e,t){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},function(e,t,n){"use strict";var r=n(2),o=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var t,n,s,i={};return e?(r.forEach(e.split("\n"),function(e){if(s=e.indexOf(":"),t=r.trim(e.substr(0,s)).toLowerCase(),n=r.trim(e.substr(s+1)),t){if(i[t]&&o.indexOf(t)>=0)return;"set-cookie"===t?i[t]=(i[t]?i[t]:[]).concat([n]):i[t]=i[t]?i[t]+", "+n:n}}),i):i}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){function e(e){var t=e;return n&&(o.setAttribute("href",t),t=o.href),o.setAttribute("href",t),{href:o.href,protocol:o.protocol?o.protocol.replace(/:$/,""):"",host:o.host,search:o.search?o.search.replace(/^\?/,""):"",hash:o.hash?o.hash.replace(/^#/,""):"",hostname:o.hostname,port:o.port,pathname:"/"===o.pathname.charAt(0)?o.pathname:"/"+o.pathname}}var t,n=/(msie|trident)/i.test(navigator.userAgent),o=document.createElement("a");return t=e(window.location.href),function(n){var o=r.isString(n)?e(n):n;return o.protocol===t.protocol&&o.host===t.host}}():function(){return function(){return!0}}()},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){function n(e,t){return r.isPlainObject(e)&&r.isPlainObject(t)?r.merge(e,t):r.isPlainObject(t)?r.merge({},t):r.isArray(t)?t.slice():t}function o(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(s[o]=n(void 0,e[o])):s[o]=n(e[o],t[o])}t=t||{};var s={},i=["url","method","data"],a=["headers","auth","proxy","params"],u=["baseURL","transformRequest","transformResponse","paramsSerializer","timeout","timeoutMessage","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","decompress","maxContentLength","maxBodyLength","maxRedirects","transport","httpAgent","httpsAgent","cancelToken","socketPath","responseEncoding"],c=["validateStatus"];r.forEach(i,function(e){r.isUndefined(t[e])||(s[e]=n(void 0,t[e]))}),r.forEach(a,o),r.forEach(u,function(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(s[o]=n(void 0,e[o])):s[o]=n(void 0,t[o])}),r.forEach(c,function(r){r in t?s[r]=n(e[r],t[r]):r in e&&(s[r]=n(void 0,e[r]))});var f=i.concat(a).concat(u).concat(c),p=Object.keys(e).concat(Object.keys(t)).filter(function(e){return f.indexOf(e)===-1});return r.forEach(p,o),s}},function(e,t){"use strict";function n(e){this.message=e}n.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},n.prototype.__CANCEL__=!0,e.exports=n},function(e,t,n){"use strict";function r(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise(function(e){t=e});var n=this;e(function(e){n.reason||(n.reason=new o(e),t(n.reason))})}var o=n(23);r.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},r.source=function(){var e,t=new r(function(t){e=t});return{token:t,cancel:e}},e.exports=r},function(e,t){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}},function(e,t){"use strict";e.exports=function(e){return"object"==typeof e&&e.isAxiosError===!0}}])}); diff --git a/html/js/external/axios-0.21.4.min.js b/html/js/external/axios-0.21.4.min.js new file mode 100644 index 00000000..d5b138ad --- /dev/null +++ b/html/js/external/axios-0.21.4.min.js @@ -0,0 +1,3 @@ +/* axios v0.21.4 | (c) 2021 by Matt Zabriskie */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(window,(function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=10)}([function(e,t,r){"use strict";var n=r(2),o=Object.prototype.toString;function i(e){return"[object Array]"===o.call(e)}function s(e){return void 0===e}function a(e){return null!==e&&"object"==typeof e}function u(e){if("[object Object]"!==o.call(e))return!1;var t=Object.getPrototypeOf(e);return null===t||t===Object.prototype}function c(e){return"[object Function]"===o.call(e)}function f(e,t){if(null!=e)if("object"!=typeof e&&(e=[e]),i(e))for(var r=0,n=e.length;r=200&&e<300}};c.headers={common:{Accept:"application/json, text/plain, */*"}},n.forEach(["delete","get","head"],(function(e){c.headers[e]={}})),n.forEach(["post","put","patch"],(function(e){c.headers[e]=n.merge(s)})),e.exports=c},function(e,t,r){"use strict";e.exports=function(e,t){return function(){for(var r=new Array(arguments.length),n=0;n=0)return;s[t]="set-cookie"===t?(s[t]?s[t]:[]).concat([r]):s[t]?s[t]+", "+r:r}})),s):s}},function(e,t,r){"use strict";var n=r(0);e.exports=n.isStandardBrowserEnv()?function(){var e,t=/(msie|trident)/i.test(navigator.userAgent),r=document.createElement("a");function o(e){var n=e;return t&&(r.setAttribute("href",n),n=r.href),r.setAttribute("href",n),{href:r.href,protocol:r.protocol?r.protocol.replace(/:$/,""):"",host:r.host,search:r.search?r.search.replace(/^\?/,""):"",hash:r.hash?r.hash.replace(/^#/,""):"",hostname:r.hostname,port:r.port,pathname:"/"===r.pathname.charAt(0)?r.pathname:"/"+r.pathname}}return e=o(window.location.href),function(t){var r=n.isString(t)?o(t):t;return r.protocol===e.protocol&&r.host===e.host}}():function(){return!0}},function(e,t,r){"use strict";var n=r(25),o={};["object","boolean","number","function","string","symbol"].forEach((function(e,t){o[e]=function(r){return typeof r===e||"a"+(t<1?"n ":" ")+e}}));var i={},s=n.version.split(".");function a(e,t){for(var r=t?t.split("."):s,n=e.split("."),o=0;o<3;o++){if(r[o]>n[o])return!0;if(r[o]0;){var i=n[o],s=t[i];if(s){var a=e[i],u=void 0===a||s(a,i,e);if(!0!==u)throw new TypeError("option "+i+" must be "+u)}else if(!0!==r)throw Error("Unknown option "+i)}},validators:o}},function(e){e.exports=JSON.parse('{"name":"axios","version":"0.21.4","description":"Promise based HTTP client for the browser and node.js","main":"index.js","scripts":{"test":"grunt test","start":"node ./sandbox/server.js","build":"NODE_ENV=production grunt build","preversion":"npm test","version":"npm run build && grunt version && git add -A dist && git add CHANGELOG.md bower.json package.json","postversion":"git push && git push --tags","examples":"node ./examples/server.js","coveralls":"cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js","fix":"eslint --fix lib/**/*.js"},"repository":{"type":"git","url":"https://github.com/axios/axios.git"},"keywords":["xhr","http","ajax","promise","node"],"author":"Matt Zabriskie","license":"MIT","bugs":{"url":"https://github.com/axios/axios/issues"},"homepage":"https://axios-http.com","devDependencies":{"coveralls":"^3.0.0","es6-promise":"^4.2.4","grunt":"^1.3.0","grunt-banner":"^0.6.0","grunt-cli":"^1.2.0","grunt-contrib-clean":"^1.1.0","grunt-contrib-watch":"^1.0.0","grunt-eslint":"^23.0.0","grunt-karma":"^4.0.0","grunt-mocha-test":"^0.13.3","grunt-ts":"^6.0.0-beta.19","grunt-webpack":"^4.0.2","istanbul-instrumenter-loader":"^1.0.0","jasmine-core":"^2.4.1","karma":"^6.3.2","karma-chrome-launcher":"^3.1.0","karma-firefox-launcher":"^2.1.0","karma-jasmine":"^1.1.1","karma-jasmine-ajax":"^0.1.13","karma-safari-launcher":"^1.0.0","karma-sauce-launcher":"^4.3.6","karma-sinon":"^1.0.5","karma-sourcemap-loader":"^0.3.8","karma-webpack":"^4.0.2","load-grunt-tasks":"^3.5.2","minimist":"^1.2.0","mocha":"^8.2.1","sinon":"^4.5.0","terser-webpack-plugin":"^4.2.3","typescript":"^4.0.5","url-search-params":"^0.10.0","webpack":"^4.44.2","webpack-dev-server":"^3.11.0"},"browser":{"./lib/adapters/http.js":"./lib/adapters/xhr.js"},"jsdelivr":"dist/axios.min.js","unpkg":"dist/axios.min.js","typings":"./index.d.ts","dependencies":{"follow-redirects":"^1.14.0"},"bundlesize":[{"path":"./dist/axios.min.js","threshold":"5kB"}]}')},function(e,t,r){"use strict";var n=r(9);function o(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise((function(e){t=e}));var r=this;e((function(e){r.reason||(r.reason=new n(e),t(r.reason))}))}o.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},o.source=function(){var e;return{token:new o((function(t){e=t})),cancel:e}},e.exports=o},function(e,t,r){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}},function(e,t,r){"use strict";e.exports=function(e){return"object"==typeof e&&!0===e.isAxiosError}}])})); +//# sourceMappingURL=axios.min.map \ No newline at end of file diff --git a/html/js/external/marked-2.0.1.min.js b/html/js/external/marked-2.0.1.min.js deleted file mode 100644 index 9ac18938..00000000 --- a/html/js/external/marked-2.0.1.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * marked - a markdown parser - * Copyright (c) 2011-2021, Christopher Jeffrey. (MIT Licensed) - * https://github.com/markedjs/marked - */ -!function(e,u){"object"==typeof exports&&"undefined"!=typeof module?module.exports=u():"function"==typeof define&&define.amd?define(u):(e="undefined"!=typeof globalThis?globalThis:e||self).marked=u()}(this,function(){"use strict";function r(e,u){for(var t=0;te.length)&&(u=e.length);for(var t=0,n=new Array(u);t=e.length?{done:!0}:{done:!1,value:e[n++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function t(e){return D[e]}var e,u=(function(u){function e(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}u.exports={defaults:e(),getDefaults:e,changeDefaults:function(e){u.exports.defaults=e}}}(e={exports:{}}),e.exports),n=/[&<>"']/,s=/[&<>"']/g,l=/[<>"']|&(?!#?\w+;)/,a=/[<>"']|&(?!#?\w+;)/g,D={"&":"&","<":"<",">":">",'"':""","'":"'"};var o=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function h(e){return e.replace(o,function(e,u){return"colon"===(u=u.toLowerCase())?":":"#"===u.charAt(0)?"x"===u.charAt(1)?String.fromCharCode(parseInt(u.substring(2),16)):String.fromCharCode(+u.substring(1)):""})}var p=/(^|[^\[])\^/g;var g=/[^\w:]/g,f=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;var F={},A=/^[^:]+:\/*[^/]*$/,C=/^([^:]+:)[\s\S]*$/,d=/^([^:]+:\/*[^/]*)[\s\S]*$/;function E(e,u){F[" "+e]||(A.test(e)?F[" "+e]=e+"/":F[" "+e]=k(e,"/",!0));var t=-1===(e=F[" "+e]).indexOf(":");return"//"===u.substring(0,2)?t?u:e.replace(C,"$1")+u:"/"===u.charAt(0)?t?u:e.replace(d,"$1")+u:e+u}function k(e,u,t){var n=e.length;if(0===n)return"";for(var r=0;ru)t.splice(u);else for(;t.length>=1,e+=e;return t+e},S=u.defaults,T=k,I=y,R=m,Z=_;function q(e,u,t){var n=u.href,r=u.title?R(u.title):null,u=e[1].replace(/\\([\[\]])/g,"$1");return"!"!==e[0].charAt(0)?{type:"link",raw:t,href:n,title:r,text:u}:{type:"image",raw:t,href:n,title:r,text:R(u)}}var O=function(){function e(e){this.options=e||S}var u=e.prototype;return u.space=function(e){e=this.rules.block.newline.exec(e);if(e)return 1=t.length?e.slice(t.length):e}).join("\n")}(t,u[3]||"");return{type:"code",raw:t,lang:u[2]&&u[2].trim(),text:e}}},u.heading=function(e){var u=this.rules.block.heading.exec(e);if(u){var t=u[2].trim();return/#$/.test(t)&&(e=T(t,"#"),!this.options.pedantic&&e&&!/ $/.test(e)||(t=e.trim())),{type:"heading",raw:u[0],depth:u[1].length,text:t}}},u.nptable=function(e){e=this.rules.block.nptable.exec(e);if(e){var u={type:"table",header:I(e[1].replace(/^ *| *\| *$/g,"")),align:e[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:e[3]?e[3].replace(/\n$/,"").split("\n"):[],raw:e[0]};if(u.header.length===u.align.length){for(var t=u.align.length,n=0;n ?/gm,"");return{type:"blockquote",raw:u[0],text:e}}},u.list=function(e){e=this.rules.block.list.exec(e);if(e){for(var u,t,n,r,i,s,l=e[0],a=e[2],D=1g[1].length:n[1].length>=g[0].length||3/i.test(e[0])&&(u=!1),!t&&/^<(pre|code|kbd|script)(\s|>)/i.test(e[0])?t=!0:t&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(e[0])&&(t=!1),{type:this.options.sanitize?"text":"html",raw:e[0],inLink:u,inRawBlock:t,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):R(e[0]):e[0]}},u.link=function(e){var u=this.rules.inline.link.exec(e);if(u){var t=u[2].trim();if(!this.options.pedantic&&/^$/.test(t))return;e=T(t.slice(0,-1),"\\");if((t.length-e.length)%2==0)return}else{var n=Z(u[2],"()");-1$/.test(t)?n.slice(1):n.slice(1,-1):n)&&n.replace(this.rules.inline._escapes,"$1"),title:i&&i.replace(this.rules.inline._escapes,"$1")},u[0])}},u.reflink=function(e,u){if((t=this.rules.inline.reflink.exec(e))||(t=this.rules.inline.nolink.exec(e))){e=(t[2]||t[1]).replace(/\s+/g," ");if((e=u[e.toLowerCase()])&&e.href)return q(t,e,t[0]);var t=t[0].charAt(0);return{type:"text",raw:t,text:t}}},u.emStrong=function(e,u,t){void 0===t&&(t="");var n=this.rules.inline.emStrong.lDelim.exec(e);if(n&&(!n[3]||!t.match(/(?:[0-9A-Za-z\xAA\xB2\xB3\xB5\xB9\xBA\xBC-\xBE\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u0660-\u0669\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08C7\u0904-\u0939\u093D\u0950\u0958-\u0961\u0966-\u096F\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09F9\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AE6-\u0AEF\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B6F\u0B71-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0BE6-\u0BF2\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C66-\u0C6F\u0C78-\u0C7E\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D58-\u0D61\u0D66-\u0D78\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DE6-\u0DEF\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F20-\u0F33\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F-\u1049\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u1090-\u1099\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1369-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A20-\u1A54\u1A80-\u1A89\u1A90-\u1A99\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B50-\u1B59\u1B83-\u1BA0\u1BAE-\u1BE5\u1C00-\u1C23\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2070\u2071\u2074-\u2079\u207F-\u2089\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2150-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2CFD\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3192-\u3195\u31A0-\u31BF\u31F0-\u31FF\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\u3400-\u4DBF\u4E00-\u9FFC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7BF\uA7C2-\uA7CA\uA7F5-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA830-\uA835\uA840-\uA873\uA882-\uA8B3\uA8D0-\uA8D9\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA900-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF-\uA9D9\uA9E0-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD07-\uDD33\uDD40-\uDD78\uDD8A\uDD8B\uDE80-\uDE9C\uDEA0-\uDED0\uDEE1-\uDEFB\uDF00-\uDF23\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC58-\uDC76\uDC79-\uDC9E\uDCA7-\uDCAF\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDD1B\uDD20-\uDD39\uDD80-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE40-\uDE48\uDE60-\uDE7E\uDE80-\uDE9F\uDEC0-\uDEC7\uDEC9-\uDEE4\uDEEB-\uDEEF\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDD23\uDD30-\uDD39\uDE60-\uDE7E\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF27\uDF30-\uDF45\uDF51-\uDF54\uDFB0-\uDFCB\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC52-\uDC6F\uDC83-\uDCAF\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD03-\uDD26\uDD36-\uDD3F\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDD0-\uDDDA\uDDDC\uDDE1-\uDDF4\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDEF0-\uDEF9\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC50-\uDC59\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE50-\uDE59\uDE80-\uDEAA\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF30-\uDF3B]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCF2\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDD50-\uDD59\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC50-\uDC6C\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF2\uDFB0\uDFC0-\uDFD4]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE96\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82C[\uDC00-\uDD1E\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD834[\uDEE0-\uDEF3\uDF60-\uDF78]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD40-\uDD49\uDD4E\uDEC0-\uDEEB\uDEF0-\uDEF9]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF\uDD00-\uDD43\uDD4B\uDD50-\uDD59]|\uD83B[\uDC71-\uDCAB\uDCAD-\uDCAF\uDCB1-\uDCB4\uDD01-\uDD2D\uDD2F-\uDD3D\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD00-\uDD0C]|\uD83E[\uDFF0-\uDFF9]|\uD869[\uDC00-\uDEDD\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])/))){var r=n[1]||n[2]||"";if(!r||r&&(""===t||this.rules.inline.punctuation.exec(t))){var i,s=n[0].length-1,l=s,a=0,D="*"===n[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(D.lastIndex=0,u=u.slice(-1*e.length+s);null!=(n=D.exec(u));)if(i=n[1]||n[2]||n[3]||n[4]||n[5]||n[6])if(i=i.length,n[3]||n[4])l+=i;else if(!((n[5]||n[6])&&s%3)||(s+i)%3){if(!(0<(l-=i))){if(l+a-i<=0&&!u.slice(D.lastIndex).match(D)&&(i=Math.min(i,i+l+a)),Math.min(s,i)%2)return{type:"em",raw:e.slice(0,s+n.index+i+1),text:e.slice(1,s+n.index+i)};if(Math.min(s,i)%2==0)return{type:"strong",raw:e.slice(0,s+n.index+i+1),text:e.slice(2,s+n.index+i-1)}}}else a+=i}}},u.codespan=function(e){var u=this.rules.inline.code.exec(e);if(u){var t=u[2].replace(/\n/g," "),n=/[^ ]/.test(t),e=/^ /.test(t)&&/ $/.test(t);return n&&e&&(t=t.substring(1,t.length-1)),t=R(t,!0),{type:"codespan",raw:u[0],text:t}}},u.br=function(e){e=this.rules.inline.br.exec(e);if(e)return{type:"br",raw:e[0]}},u.del=function(e){e=this.rules.inline.del.exec(e);if(e)return{type:"del",raw:e[0],text:e[2]}},u.autolink=function(e,u){e=this.rules.inline.autolink.exec(e);if(e){var t,u="@"===e[2]?"mailto:"+(t=R(this.options.mangle?u(e[1]):e[1])):t=R(e[1]);return{type:"link",raw:e[0],text:t,href:u,tokens:[{type:"text",raw:t,text:t}]}}},u.url=function(e,u){var t,n,r,i;if(t=this.rules.inline.url.exec(e)){if("@"===t[2])r="mailto:"+(n=R(this.options.mangle?u(t[0]):t[0]));else{for(;i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])[0],i!==t[0];);n=R(t[0]),r="www."===t[1]?"http://"+n:n}return{type:"link",raw:t[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}},u.inlineText=function(e,u,t){e=this.rules.inline.text.exec(e);if(e){t=u?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):R(e[0]):e[0]:R(this.options.smartypants?t(e[0]):e[0]);return{type:"text",raw:e[0],text:t}}},e}(),y=w,_=x,w=v,x={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?! {0,3}bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:\\n{2,}|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,nptable:y,table:y,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};x.def=_(x.def).replace("label",x._label).replace("title",x._title).getRegex(),x.bullet=/(?:[*+-]|\d{1,9}[.)])/,x.item=/^( *)(bull) ?[^\n]*(?:\n(?! *bull ?)[^\n]*)*/,x.item=_(x.item,"gm").replace(/bull/g,x.bullet).getRegex(),x.listItemStart=_(/^( *)(bull) */).replace("bull",x.bullet).getRegex(),x.list=_(x.list).replace(/bull/g,x.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+x.def.source+")").getRegex(),x._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",x._comment=/|$)/,x.html=_(x.html,"i").replace("comment",x._comment).replace("tag",x._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),x.paragraph=_(x._paragraph).replace("hr",x.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",x._tag).getRegex(),x.blockquote=_(x.blockquote).replace("paragraph",x.paragraph).getRegex(),x.normal=w({},x),x.gfm=w({},x.normal,{nptable:"^ *([^|\\n ].*\\|.*)\\n {0,3}([-:]+ *\\|[-| :]*)(?:\\n((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)",table:"^ *\\|(.+)\\n {0,3}\\|?( *[-:]+[-| :]*)(?:\\n *((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),x.gfm.nptable=_(x.gfm.nptable).replace("hr",x.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",x._tag).getRegex(),x.gfm.table=_(x.gfm.table).replace("hr",x.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",x._tag).getRegex(),x.pedantic=w({},x.normal,{html:_("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",x._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:y,paragraph:_(x.normal._paragraph).replace("hr",x.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",x.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()});y={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:y,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/\_\_[^_]*?\*[^_]*?\_\_|[punct_](\*+)(?=[\s]|$)|[^punct*_\s](\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|[^punct*_\s](\*+)(?=[^punct*_\s])/,rDelimUnd:/\*\*[^*]*?\_[^*]*?\*\*|[punct*](\_+)(?=[\s]|$)|[^punct*_\s](\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:y,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~"};y.punctuation=_(y.punctuation).replace(/punctuation/g,y._punctuation).getRegex(),y.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,y.escapedEmSt=/\\\*|\\_/g,y._comment=_(x._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),y.emStrong.lDelim=_(y.emStrong.lDelim).replace(/punct/g,y._punctuation).getRegex(),y.emStrong.rDelimAst=_(y.emStrong.rDelimAst,"g").replace(/punct/g,y._punctuation).getRegex(),y.emStrong.rDelimUnd=_(y.emStrong.rDelimUnd,"g").replace(/punct/g,y._punctuation).getRegex(),y._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,y._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,y._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,y.autolink=_(y.autolink).replace("scheme",y._scheme).replace("email",y._email).getRegex(),y._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,y.tag=_(y.tag).replace("comment",y._comment).replace("attribute",y._attribute).getRegex(),y._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,y._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,y._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,y.link=_(y.link).replace("label",y._label).replace("href",y._href).replace("title",y._title).getRegex(),y.reflink=_(y.reflink).replace("label",y._label).getRegex(),y.reflinkSearch=_(y.reflinkSearch,"g").replace("reflink",y.reflink).replace("nolink",y.nolink).getRegex(),y.normal=w({},y),y.pedantic=w({},y.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:_(/^!?\[(label)\]\((.*?)\)/).replace("label",y._label).getRegex(),reflink:_(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",y._label).getRegex()}),y.gfm=w({},y.normal,{escape:_(y.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\'+(t?e:H(e,!0))+"\n":"
"+(t?e:H(e,!0))+"
\n"},u.blockquote=function(e){return"
\n"+e+"
\n"},u.html=function(e){return e},u.heading=function(e,u,t,n){return this.options.headerIds?"'+e+"\n":""+e+"\n"},u.hr=function(){return this.options.xhtml?"
\n":"
\n"},u.list=function(e,u,t){var n=u?"ol":"ul";return"<"+n+(u&&1!==t?' start="'+t+'"':"")+">\n"+e+"\n"},u.listitem=function(e){return"
  • "+e+"
  • \n"},u.checkbox=function(e){return" "},u.paragraph=function(e){return"

    "+e+"

    \n"},u.table=function(e,u){return"\n\n"+e+"\n"+(u=u&&""+u+"")+"
    \n"},u.tablerow=function(e){return"\n"+e+"\n"},u.tablecell=function(e,u){var t=u.header?"th":"td";return(u.align?"<"+t+' align="'+u.align+'">':"<"+t+">")+e+"\n"},u.strong=function(e){return""+e+""},u.em=function(e){return""+e+""},u.codespan=function(e){return""+e+""},u.br=function(){return this.options.xhtml?"
    ":"
    "},u.del=function(e){return""+e+""},u.link=function(e,u,t){if(null===(e=V(this.options.sanitize,this.options.baseUrl,e)))return t;e='
    "},u.image=function(e,u,t){if(null===(e=V(this.options.sanitize,this.options.baseUrl,e)))return t;t=''+t+'":">"},u.text=function(e){return e},e}(),K=function(){function e(){}var u=e.prototype;return u.strong=function(e){return e},u.em=function(e){return e},u.codespan=function(e){return e},u.del=function(e){return e},u.html=function(e){return e},u.text=function(e){return e},u.link=function(e,u,t){return""+t},u.image=function(e,u,t){return""+t},u.br=function(){return""},e}(),Q=function(){function e(){this.seen={}}var u=e.prototype;return u.serialize=function(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")},u.getNextSafeSlug=function(e,u){var t=e,n=0;if(this.seen.hasOwnProperty(t))for(n=this.seen[e];t=e+"-"+ ++n,this.seen.hasOwnProperty(t););return u||(this.seen[e]=n,this.seen[t]=0),t},u.slug=function(e,u){void 0===u&&(u={});var t=this.serialize(e);return this.getNextSafeSlug(t,u.dryrun)},e}(),W=u.defaults,Y=b,ee=function(){function t(e){this.options=e||W,this.options.renderer=this.options.renderer||new J,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new K,this.slugger=new Q}t.parse=function(e,u){return new t(u).parse(e)},t.parseInline=function(e,u){return new t(u).parseInline(e)};var e=t.prototype;return e.parse=function(e,u){void 0===u&&(u=!0);for(var t,n,r,i,s,l,a,D,o,c,h,p,g,f,F,A="",C=e.length,d=0;dAn error occurred:

    "+ne(e.message+"",!0)+"
    ";throw e}}return ie.options=ie.setOptions=function(e){return ue(ie.defaults,e),re(ie.defaults),ie},ie.getDefaults=m,ie.defaults=u,ie.use=function(l){var u,t=ue({},l);l.renderer&&function(){var e,s=ie.defaults.renderer||new J;for(e in l.renderer)!function(r){var i=s[r];s[r]=function(){for(var e=arguments.length,u=new Array(e),t=0;tAn error occurred:

    "+ne(e.message+"",!0)+"
    ";throw e}},ie.Parser=ee,ie.parser=ee.parse,ie.Renderer=J,ie.TextRenderer=K,ie.Lexer=X,ie.lexer=X.lex,ie.Tokenizer=O,ie.Slugger=Q,ie.parse=ie}); \ No newline at end of file diff --git a/html/js/external/marked-2.1.3.min.js b/html/js/external/marked-2.1.3.min.js new file mode 100644 index 00000000..a302c283 --- /dev/null +++ b/html/js/external/marked-2.1.3.min.js @@ -0,0 +1,6 @@ +/** + * marked - a markdown parser + * Copyright (c) 2011-2021, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).marked=t()}(this,function(){"use strict";function r(e,t){for(var u=0;ue.length)&&(t=e.length);for(var u=0,n=new Array(t);u=e.length?{done:!0}:{done:!1,value:e[n++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var t={exports:{}};function e(){return{baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}t.exports={defaults:e(),getDefaults:e,changeDefaults:function(e){t.exports.defaults=e}};function u(e){return D[e]}var n=/[&<>"']/,s=/[&<>"']/g,l=/[<>"']|&(?!#?\w+;)/,a=/[<>"']|&(?!#?\w+;)/g,D={"&":"&","<":"<",">":">",'"':""","'":"'"};var c=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function h(e){return e.replace(c,function(e,t){return"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}var p=/(^|[^\[])\^/g;var g=/[^\w:]/g,f=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;var F={},A=/^[^:]+:\/*[^/]*$/,d=/^([^:]+:)[\s\S]*$/,C=/^([^:]+:\/*[^/]*)[\s\S]*$/;function k(e,t){F[" "+e]||(A.test(e)?F[" "+e]=e+"/":F[" "+e]=E(e,"/",!0));var u=-1===(e=F[" "+e]).indexOf(":");return"//"===t.substring(0,2)?u?t:e.replace(d,"$1")+t:"/"===t.charAt(0)?u?t:e.replace(C,"$1")+t:e+t}function E(e,t,u){var n=e.length;if(0===n)return"";for(var r=0;rt)u.splice(t);else for(;u.length>=1,e+=e;return u+e},T=t.exports.defaults,I=_,R=y,q=x,Z=z;function O(e,t,u){var n=t.href,r=t.title?q(t.title):null,t=e[1].replace(/\\([\[\]])/g,"$1");return"!"!==e[0].charAt(0)?{type:"link",raw:u,href:n,title:r,text:t}:{type:"image",raw:u,href:n,title:r,text:q(t)}}_=function(){function e(e){this.options=e||T}var t=e.prototype;return t.space=function(e){e=this.rules.block.newline.exec(e);if(e)return 1=u.length?e.slice(u.length):e}).join("\n")}(u,t[3]||"");return{type:"code",raw:u,lang:t[2]&&t[2].trim(),text:e}}},t.heading=function(e){var t=this.rules.block.heading.exec(e);if(t){var u=t[2].trim();return/#$/.test(u)&&(e=I(u,"#"),!this.options.pedantic&&e&&!/ $/.test(e)||(u=e.trim())),{type:"heading",raw:t[0],depth:t[1].length,text:u}}},t.nptable=function(e){e=this.rules.block.nptable.exec(e);if(e){var t={type:"table",header:R(e[1].replace(/^ *| *\| *$/g,"")),align:e[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:e[3]?e[3].replace(/\n$/,"").split("\n"):[],raw:e[0]};if(t.header.length===t.align.length){for(var u=t.align.length,n=0;n ?/gm,"");return{type:"blockquote",raw:t[0],text:e}}},t.list=function(e){e=this.rules.block.list.exec(e);if(e){for(var t,u,n,r,i,s,l=e[0],a=e[2],o=1g[1].length:n[1].length>=g[0].length||3/i.test(e[0])&&(t=!1),!u&&/^<(pre|code|kbd|script)(\s|>)/i.test(e[0])?u=!0:u&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(e[0])&&(u=!1),{type:this.options.sanitize?"text":"html",raw:e[0],inLink:t,inRawBlock:u,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):q(e[0]):e[0]}},t.link=function(e){var t=this.rules.inline.link.exec(e);if(t){var u=t[2].trim();if(!this.options.pedantic&&/^$/.test(u))return;e=I(u.slice(0,-1),"\\");if((u.length-e.length)%2==0)return}else{var n=Z(t[2],"()");-1$/.test(u)?n.slice(1):n.slice(1,-1):n)&&n.replace(this.rules.inline._escapes,"$1"),title:i&&i.replace(this.rules.inline._escapes,"$1")},t[0])}},t.reflink=function(e,t){if((u=this.rules.inline.reflink.exec(e))||(u=this.rules.inline.nolink.exec(e))){e=(u[2]||u[1]).replace(/\s+/g," ");if((e=t[e.toLowerCase()])&&e.href)return O(u,e,u[0]);var u=u[0].charAt(0);return{type:"text",raw:u,text:u}}},t.emStrong=function(e,t,u){void 0===u&&(u="");var n=this.rules.inline.emStrong.lDelim.exec(e);if(n&&(!n[3]||!u.match(/(?:[0-9A-Za-z\xAA\xB2\xB3\xB5\xB9\xBA\xBC-\xBE\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u0660-\u0669\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08C7\u0904-\u0939\u093D\u0950\u0958-\u0961\u0966-\u096F\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09F9\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AE6-\u0AEF\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B6F\u0B71-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0BE6-\u0BF2\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C66-\u0C6F\u0C78-\u0C7E\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D58-\u0D61\u0D66-\u0D78\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DE6-\u0DEF\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F20-\u0F33\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F-\u1049\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u1090-\u1099\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1369-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A20-\u1A54\u1A80-\u1A89\u1A90-\u1A99\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B50-\u1B59\u1B83-\u1BA0\u1BAE-\u1BE5\u1C00-\u1C23\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2070\u2071\u2074-\u2079\u207F-\u2089\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2150-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2CFD\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3192-\u3195\u31A0-\u31BF\u31F0-\u31FF\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\u3400-\u4DBF\u4E00-\u9FFC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7BF\uA7C2-\uA7CA\uA7F5-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA830-\uA835\uA840-\uA873\uA882-\uA8B3\uA8D0-\uA8D9\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA900-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF-\uA9D9\uA9E0-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD07-\uDD33\uDD40-\uDD78\uDD8A\uDD8B\uDE80-\uDE9C\uDEA0-\uDED0\uDEE1-\uDEFB\uDF00-\uDF23\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC58-\uDC76\uDC79-\uDC9E\uDCA7-\uDCAF\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDD1B\uDD20-\uDD39\uDD80-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE40-\uDE48\uDE60-\uDE7E\uDE80-\uDE9F\uDEC0-\uDEC7\uDEC9-\uDEE4\uDEEB-\uDEEF\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDD23\uDD30-\uDD39\uDE60-\uDE7E\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF27\uDF30-\uDF45\uDF51-\uDF54\uDFB0-\uDFCB\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC52-\uDC6F\uDC83-\uDCAF\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD03-\uDD26\uDD36-\uDD3F\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDD0-\uDDDA\uDDDC\uDDE1-\uDDF4\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDEF0-\uDEF9\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC50-\uDC59\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE50-\uDE59\uDE80-\uDEAA\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF30-\uDF3B]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCF2\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDD50-\uDD59\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC50-\uDC6C\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF2\uDFB0\uDFC0-\uDFD4]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE96\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82C[\uDC00-\uDD1E\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD834[\uDEE0-\uDEF3\uDF60-\uDF78]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD40-\uDD49\uDD4E\uDEC0-\uDEEB\uDEF0-\uDEF9]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF\uDD00-\uDD43\uDD4B\uDD50-\uDD59]|\uD83B[\uDC71-\uDCAB\uDCAD-\uDCAF\uDCB1-\uDCB4\uDD01-\uDD2D\uDD2F-\uDD3D\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD00-\uDD0C]|\uD83E[\uDFF0-\uDFF9]|\uD869[\uDC00-\uDEDD\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])/))){var r=n[1]||n[2]||"";if(!r||r&&(""===u||this.rules.inline.punctuation.exec(u))){var i,s=n[0].length-1,l=s,a=0,o="*"===n[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(o.lastIndex=0,t=t.slice(-1*e.length+s);null!=(n=o.exec(t));)if(i=n[1]||n[2]||n[3]||n[4]||n[5]||n[6])if(i=i.length,n[3]||n[4])l+=i;else if(!((n[5]||n[6])&&s%3)||(s+i)%3){if(!(0<(l-=i)))return i=Math.min(i,i+l+a),Math.min(s,i)%2?{type:"em",raw:e.slice(0,s+n.index+i+1),text:e.slice(1,s+n.index+i)}:{type:"strong",raw:e.slice(0,s+n.index+i+1),text:e.slice(2,s+n.index+i-1)}}else a+=i}}},t.codespan=function(e){var t=this.rules.inline.code.exec(e);if(t){var u=t[2].replace(/\n/g," "),n=/[^ ]/.test(u),e=/^ /.test(u)&&/ $/.test(u);return n&&e&&(u=u.substring(1,u.length-1)),u=q(u,!0),{type:"codespan",raw:t[0],text:u}}},t.br=function(e){e=this.rules.inline.br.exec(e);if(e)return{type:"br",raw:e[0]}},t.del=function(e){e=this.rules.inline.del.exec(e);if(e)return{type:"del",raw:e[0],text:e[2]}},t.autolink=function(e,t){e=this.rules.inline.autolink.exec(e);if(e){var u,t="@"===e[2]?"mailto:"+(u=q(this.options.mangle?t(e[1]):e[1])):u=q(e[1]);return{type:"link",raw:e[0],text:u,href:t,tokens:[{type:"text",raw:u,text:u}]}}},t.url=function(e,t){var u,n,r,i;if(u=this.rules.inline.url.exec(e)){if("@"===u[2])r="mailto:"+(n=q(this.options.mangle?t(u[0]):u[0]));else{for(;i=u[0],u[0]=this.rules.inline._backpedal.exec(u[0])[0],i!==u[0];);n=q(u[0]),r="www."===u[1]?"http://"+n:n}return{type:"link",raw:u[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}},t.inlineText=function(e,t,u){e=this.rules.inline.text.exec(e);if(e){u=t?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):q(e[0]):e[0]:q(this.options.smartypants?u(e[0]):e[0]);return{type:"text",raw:e[0],text:u}}},e}(),y=w,z=b,w=v,b={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?! {0,3}bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,nptable:y,table:y,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};b.def=z(b.def).replace("label",b._label).replace("title",b._title).getRegex(),b.bullet=/(?:[*+-]|\d{1,9}[.)])/,b.item=/^( *)(bull) ?[^\n]*(?:\n(?! *bull ?)[^\n]*)*/,b.item=z(b.item,"gm").replace(/bull/g,b.bullet).getRegex(),b.listItemStart=z(/^( *)(bull) */).replace("bull",b.bullet).getRegex(),b.list=z(b.list).replace(/bull/g,b.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+b.def.source+")").getRegex(),b._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",b._comment=/|$)/,b.html=z(b.html,"i").replace("comment",b._comment).replace("tag",b._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),b.paragraph=z(b._paragraph).replace("hr",b.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",b._tag).getRegex(),b.blockquote=z(b.blockquote).replace("paragraph",b.paragraph).getRegex(),b.normal=w({},b),b.gfm=w({},b.normal,{nptable:"^ *([^|\\n ].*\\|.*)\\n {0,3}([-:]+ *\\|[-| :]*)(?:\\n((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)",table:"^ *\\|(.+)\\n {0,3}\\|?( *[-:]+[-| :]*)(?:\\n *((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),b.gfm.nptable=z(b.gfm.nptable).replace("hr",b.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",b._tag).getRegex(),b.gfm.table=z(b.gfm.table).replace("hr",b.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",b._tag).getRegex(),b.pedantic=w({},b.normal,{html:z("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",b._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:y,paragraph:z(b.normal._paragraph).replace("hr",b.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",b.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()});y={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:y,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/\_\_[^_*]*?\*[^_*]*?\_\_|[punct_](\*+)(?=[\s]|$)|[^punct*_\s](\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|[^punct*_\s](\*+)(?=[^punct*_\s])/,rDelimUnd:/\*\*[^_*]*?\_[^_*]*?\*\*|[punct*](\_+)(?=[\s]|$)|[^punct*_\s](\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:y,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~"};y.punctuation=z(y.punctuation).replace(/punctuation/g,y._punctuation).getRegex(),y.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,y.escapedEmSt=/\\\*|\\_/g,y._comment=z(b._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),y.emStrong.lDelim=z(y.emStrong.lDelim).replace(/punct/g,y._punctuation).getRegex(),y.emStrong.rDelimAst=z(y.emStrong.rDelimAst,"g").replace(/punct/g,y._punctuation).getRegex(),y.emStrong.rDelimUnd=z(y.emStrong.rDelimUnd,"g").replace(/punct/g,y._punctuation).getRegex(),y._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,y._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,y._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,y.autolink=z(y.autolink).replace("scheme",y._scheme).replace("email",y._email).getRegex(),y._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,y.tag=z(y.tag).replace("comment",y._comment).replace("attribute",y._attribute).getRegex(),y._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,y._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,y._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,y.link=z(y.link).replace("label",y._label).replace("href",y._href).replace("title",y._title).getRegex(),y.reflink=z(y.reflink).replace("label",y._label).getRegex(),y.reflinkSearch=z(y.reflinkSearch,"g").replace("reflink",y.reflink).replace("nolink",y.nolink).getRegex(),y.normal=w({},y),y.pedantic=w({},y.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:z(/^!?\[(label)\]\((.*?)\)/).replace("label",y._label).getRegex(),reflink:z(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",y._label).getRegex()}),y.gfm=w({},y.normal,{escape:z(y.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\'+(u?e:H(e,!0))+"\n":"
    "+(u?e:H(e,!0))+"
    \n"},t.blockquote=function(e){return"
    \n"+e+"
    \n"},t.html=function(e){return e},t.heading=function(e,t,u,n){return this.options.headerIds?"'+e+"\n":""+e+"\n"},t.hr=function(){return this.options.xhtml?"
    \n":"
    \n"},t.list=function(e,t,u){var n=t?"ol":"ul";return"<"+n+(t&&1!==u?' start="'+u+'"':"")+">\n"+e+"\n"},t.listitem=function(e){return"
  • "+e+"
  • \n"},t.checkbox=function(e){return" "},t.paragraph=function(e){return"

    "+e+"

    \n"},t.table=function(e,t){return"\n\n"+e+"\n"+(t=t&&""+t+"")+"
    \n"},t.tablerow=function(e){return"\n"+e+"\n"},t.tablecell=function(e,t){var u=t.header?"th":"td";return(t.align?"<"+u+' align="'+t.align+'">':"<"+u+">")+e+"\n"},t.strong=function(e){return""+e+""},t.em=function(e){return""+e+""},t.codespan=function(e){return""+e+""},t.br=function(){return this.options.xhtml?"
    ":"
    "},t.del=function(e){return""+e+""},t.link=function(e,t,u){if(null===(e=V(this.options.sanitize,this.options.baseUrl,e)))return u;e='
    "},t.image=function(e,t,u){if(null===(e=V(this.options.sanitize,this.options.baseUrl,e)))return u;u=''+u+'":">"},t.text=function(e){return e},e}(),S=function(){function e(){}var t=e.prototype;return t.strong=function(e){return e},t.em=function(e){return e},t.codespan=function(e){return e},t.del=function(e){return e},t.html=function(e){return e},t.text=function(e){return e},t.link=function(e,t,u){return""+u},t.image=function(e,t,u){return""+u},t.br=function(){return""},e}(),B=function(){function e(){this.seen={}}var t=e.prototype;return t.serialize=function(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")},t.getNextSafeSlug=function(e,t){var u=e,n=0;if(this.seen.hasOwnProperty(u))for(n=this.seen[e];u=e+"-"+ ++n,this.seen.hasOwnProperty(u););return t||(this.seen[e]=n,this.seen[u]=0),u},t.slug=function(e,t){void 0===t&&(t={});var u=this.serialize(e);return this.getNextSafeSlug(u,t.dryrun)},e}(),J=b,K=S,Q=B,W=t.exports.defaults,Y=m,ee=y,te=function(){function u(e){this.options=e||W,this.options.renderer=this.options.renderer||new J,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new K,this.slugger=new Q}u.parse=function(e,t){return new u(t).parse(e)},u.parseInline=function(e,t){return new u(t).parseInline(e)};var e=u.prototype;return e.parse=function(e,t){void 0===t&&(t=!0);for(var u,n,r,i,s,l,a,o,D,c,h,p,g,f,F,A,d="",C=e.length,k=0;kAn error occurred:

    "+se(e.message+"",!0)+"
    ";throw e}}return ae.options=ae.setOptions=function(e){return re(ae.defaults,e),le(ae.defaults),ae},ae.getDefaults=$,ae.defaults=x,ae.use=function(){for(var u=this,e=arguments.length,t=new Array(e),n=0;nAn error occurred:

    "+se(e.message+"",!0)+"
    ";throw e}},ae.Parser=te,ae.parser=te.parse,ae.Renderer=ne,ae.TextRenderer=S,ae.Lexer=ee,ae.lexer=ee.lex,ae.Tokenizer=ue,ae.Slugger=B,ae.parse=ae}); diff --git a/html/js/external/moment-timezone-data-0.5.33.min.js b/html/js/external/moment-timezone-with-data-0.5.33.min.js similarity index 100% rename from html/js/external/moment-timezone-data-0.5.33.min.js rename to html/js/external/moment-timezone-with-data-0.5.33.min.js diff --git a/html/js/external/vue-2.6.11.min.js b/html/js/external/vue-2.6.11.min.js deleted file mode 100644 index 05e21102..00000000 --- a/html/js/external/vue-2.6.11.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Vue.js v2.6.11 - * (c) 2014-2019 Evan You - * Released under the MIT License. - */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).Vue=t()}(this,function(){"use strict";var e=Object.freeze({});function t(e){return null==e}function n(e){return null!=e}function r(e){return!0===e}function i(e){return"string"==typeof e||"number"==typeof e||"symbol"==typeof e||"boolean"==typeof e}function o(e){return null!==e&&"object"==typeof e}var a=Object.prototype.toString;function s(e){return"[object Object]"===a.call(e)}function c(e){var t=parseFloat(String(e));return t>=0&&Math.floor(t)===t&&isFinite(e)}function u(e){return n(e)&&"function"==typeof e.then&&"function"==typeof e.catch}function l(e){return null==e?"":Array.isArray(e)||s(e)&&e.toString===a?JSON.stringify(e,null,2):String(e)}function f(e){var t=parseFloat(e);return isNaN(t)?e:t}function p(e,t){for(var n=Object.create(null),r=e.split(","),i=0;i-1)return e.splice(n,1)}}var m=Object.prototype.hasOwnProperty;function y(e,t){return m.call(e,t)}function g(e){var t=Object.create(null);return function(n){return t[n]||(t[n]=e(n))}}var _=/-(\w)/g,b=g(function(e){return e.replace(_,function(e,t){return t?t.toUpperCase():""})}),$=g(function(e){return e.charAt(0).toUpperCase()+e.slice(1)}),w=/\B([A-Z])/g,C=g(function(e){return e.replace(w,"-$1").toLowerCase()});var x=Function.prototype.bind?function(e,t){return e.bind(t)}:function(e,t){function n(n){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n};function k(e,t){t=t||0;for(var n=e.length-t,r=new Array(n);n--;)r[n]=e[n+t];return r}function A(e,t){for(var n in t)e[n]=t[n];return e}function O(e){for(var t={},n=0;n0,Z=J&&J.indexOf("edge/")>0,G=(J&&J.indexOf("android"),J&&/iphone|ipad|ipod|ios/.test(J)||"ios"===K),X=(J&&/chrome\/\d+/.test(J),J&&/phantomjs/.test(J),J&&J.match(/firefox\/(\d+)/)),Y={}.watch,Q=!1;if(z)try{var ee={};Object.defineProperty(ee,"passive",{get:function(){Q=!0}}),window.addEventListener("test-passive",null,ee)}catch(e){}var te=function(){return void 0===B&&(B=!z&&!V&&"undefined"!=typeof global&&(global.process&&"server"===global.process.env.VUE_ENV)),B},ne=z&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function re(e){return"function"==typeof e&&/native code/.test(e.toString())}var ie,oe="undefined"!=typeof Symbol&&re(Symbol)&&"undefined"!=typeof Reflect&&re(Reflect.ownKeys);ie="undefined"!=typeof Set&&re(Set)?Set:function(){function e(){this.set=Object.create(null)}return e.prototype.has=function(e){return!0===this.set[e]},e.prototype.add=function(e){this.set[e]=!0},e.prototype.clear=function(){this.set=Object.create(null)},e}();var ae=S,se=0,ce=function(){this.id=se++,this.subs=[]};ce.prototype.addSub=function(e){this.subs.push(e)},ce.prototype.removeSub=function(e){h(this.subs,e)},ce.prototype.depend=function(){ce.target&&ce.target.addDep(this)},ce.prototype.notify=function(){for(var e=this.subs.slice(),t=0,n=e.length;t-1)if(o&&!y(i,"default"))a=!1;else if(""===a||a===C(e)){var c=Pe(String,i.type);(c<0||s0&&(st((u=e(u,(a||"")+"_"+c))[0])&&st(f)&&(s[l]=he(f.text+u[0].text),u.shift()),s.push.apply(s,u)):i(u)?st(f)?s[l]=he(f.text+u):""!==u&&s.push(he(u)):st(u)&&st(f)?s[l]=he(f.text+u.text):(r(o._isVList)&&n(u.tag)&&t(u.key)&&n(a)&&(u.key="__vlist"+a+"_"+c+"__"),s.push(u)));return s}(e):void 0}function st(e){return n(e)&&n(e.text)&&!1===e.isComment}function ct(e,t){if(e){for(var n=Object.create(null),r=oe?Reflect.ownKeys(e):Object.keys(e),i=0;i0,a=t?!!t.$stable:!o,s=t&&t.$key;if(t){if(t._normalized)return t._normalized;if(a&&r&&r!==e&&s===r.$key&&!o&&!r.$hasNormal)return r;for(var c in i={},t)t[c]&&"$"!==c[0]&&(i[c]=pt(n,c,t[c]))}else i={};for(var u in n)u in i||(i[u]=dt(n,u));return t&&Object.isExtensible(t)&&(t._normalized=i),R(i,"$stable",a),R(i,"$key",s),R(i,"$hasNormal",o),i}function pt(e,t,n){var r=function(){var e=arguments.length?n.apply(null,arguments):n({});return(e=e&&"object"==typeof e&&!Array.isArray(e)?[e]:at(e))&&(0===e.length||1===e.length&&e[0].isComment)?void 0:e};return n.proxy&&Object.defineProperty(e,t,{get:r,enumerable:!0,configurable:!0}),r}function dt(e,t){return function(){return e[t]}}function vt(e,t){var r,i,a,s,c;if(Array.isArray(e)||"string"==typeof e)for(r=new Array(e.length),i=0,a=e.length;idocument.createEvent("Event").timeStamp&&(sn=function(){return cn.now()})}function un(){var e,t;for(an=sn(),rn=!0,Qt.sort(function(e,t){return e.id-t.id}),on=0;onon&&Qt[n].id>e.id;)n--;Qt.splice(n+1,0,e)}else Qt.push(e);nn||(nn=!0,Ye(un))}}(this)},fn.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||o(e)||this.deep){var t=this.value;if(this.value=e,this.user)try{this.cb.call(this.vm,e,t)}catch(e){Re(e,this.vm,'callback for watcher "'+this.expression+'"')}else this.cb.call(this.vm,e,t)}}},fn.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},fn.prototype.depend=function(){for(var e=this.deps.length;e--;)this.deps[e].depend()},fn.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||h(this.vm._watchers,this);for(var e=this.deps.length;e--;)this.deps[e].removeSub(this);this.active=!1}};var pn={enumerable:!0,configurable:!0,get:S,set:S};function dn(e,t,n){pn.get=function(){return this[t][n]},pn.set=function(e){this[t][n]=e},Object.defineProperty(e,n,pn)}function vn(e){e._watchers=[];var t=e.$options;t.props&&function(e,t){var n=e.$options.propsData||{},r=e._props={},i=e.$options._propKeys=[];e.$parent&&$e(!1);var o=function(o){i.push(o);var a=Me(o,t,n,e);xe(r,o,a),o in e||dn(e,"_props",o)};for(var a in t)o(a);$e(!0)}(e,t.props),t.methods&&function(e,t){e.$options.props;for(var n in t)e[n]="function"!=typeof t[n]?S:x(t[n],e)}(e,t.methods),t.data?function(e){var t=e.$options.data;s(t=e._data="function"==typeof t?function(e,t){le();try{return e.call(t,t)}catch(e){return Re(e,t,"data()"),{}}finally{fe()}}(t,e):t||{})||(t={});var n=Object.keys(t),r=e.$options.props,i=(e.$options.methods,n.length);for(;i--;){var o=n[i];r&&y(r,o)||(a=void 0,36!==(a=(o+"").charCodeAt(0))&&95!==a&&dn(e,"_data",o))}var a;Ce(t,!0)}(e):Ce(e._data={},!0),t.computed&&function(e,t){var n=e._computedWatchers=Object.create(null),r=te();for(var i in t){var o=t[i],a="function"==typeof o?o:o.get;r||(n[i]=new fn(e,a||S,S,hn)),i in e||mn(e,i,o)}}(e,t.computed),t.watch&&t.watch!==Y&&function(e,t){for(var n in t){var r=t[n];if(Array.isArray(r))for(var i=0;i-1:"string"==typeof e?e.split(",").indexOf(t)>-1:(n=e,"[object RegExp]"===a.call(n)&&e.test(t));var n}function An(e,t){var n=e.cache,r=e.keys,i=e._vnode;for(var o in n){var a=n[o];if(a){var s=xn(a.componentOptions);s&&!t(s)&&On(n,o,r,i)}}}function On(e,t,n,r){var i=e[t];!i||r&&i.tag===r.tag||i.componentInstance.$destroy(),e[t]=null,h(n,t)}!function(t){t.prototype._init=function(t){var n=this;n._uid=bn++,n._isVue=!0,t&&t._isComponent?function(e,t){var n=e.$options=Object.create(e.constructor.options),r=t._parentVnode;n.parent=t.parent,n._parentVnode=r;var i=r.componentOptions;n.propsData=i.propsData,n._parentListeners=i.listeners,n._renderChildren=i.children,n._componentTag=i.tag,t.render&&(n.render=t.render,n.staticRenderFns=t.staticRenderFns)}(n,t):n.$options=De($n(n.constructor),t||{},n),n._renderProxy=n,n._self=n,function(e){var t=e.$options,n=t.parent;if(n&&!t.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(e)}e.$parent=n,e.$root=n?n.$root:e,e.$children=[],e.$refs={},e._watcher=null,e._inactive=null,e._directInactive=!1,e._isMounted=!1,e._isDestroyed=!1,e._isBeingDestroyed=!1}(n),function(e){e._events=Object.create(null),e._hasHookEvent=!1;var t=e.$options._parentListeners;t&&qt(e,t)}(n),function(t){t._vnode=null,t._staticTrees=null;var n=t.$options,r=t.$vnode=n._parentVnode,i=r&&r.context;t.$slots=ut(n._renderChildren,i),t.$scopedSlots=e,t._c=function(e,n,r,i){return Pt(t,e,n,r,i,!1)},t.$createElement=function(e,n,r,i){return Pt(t,e,n,r,i,!0)};var o=r&&r.data;xe(t,"$attrs",o&&o.attrs||e,null,!0),xe(t,"$listeners",n._parentListeners||e,null,!0)}(n),Yt(n,"beforeCreate"),function(e){var t=ct(e.$options.inject,e);t&&($e(!1),Object.keys(t).forEach(function(n){xe(e,n,t[n])}),$e(!0))}(n),vn(n),function(e){var t=e.$options.provide;t&&(e._provided="function"==typeof t?t.call(e):t)}(n),Yt(n,"created"),n.$options.el&&n.$mount(n.$options.el)}}(wn),function(e){var t={get:function(){return this._data}},n={get:function(){return this._props}};Object.defineProperty(e.prototype,"$data",t),Object.defineProperty(e.prototype,"$props",n),e.prototype.$set=ke,e.prototype.$delete=Ae,e.prototype.$watch=function(e,t,n){if(s(t))return _n(this,e,t,n);(n=n||{}).user=!0;var r=new fn(this,e,t,n);if(n.immediate)try{t.call(this,r.value)}catch(e){Re(e,this,'callback for immediate watcher "'+r.expression+'"')}return function(){r.teardown()}}}(wn),function(e){var t=/^hook:/;e.prototype.$on=function(e,n){var r=this;if(Array.isArray(e))for(var i=0,o=e.length;i1?k(t):t;for(var n=k(arguments,1),r='event handler for "'+e+'"',i=0,o=t.length;iparseInt(this.max)&&On(a,s[0],s,this._vnode)),t.data.keepAlive=!0}return t||e&&e[0]}}};!function(e){var t={get:function(){return F}};Object.defineProperty(e,"config",t),e.util={warn:ae,extend:A,mergeOptions:De,defineReactive:xe},e.set=ke,e.delete=Ae,e.nextTick=Ye,e.observable=function(e){return Ce(e),e},e.options=Object.create(null),M.forEach(function(t){e.options[t+"s"]=Object.create(null)}),e.options._base=e,A(e.options.components,Tn),function(e){e.use=function(e){var t=this._installedPlugins||(this._installedPlugins=[]);if(t.indexOf(e)>-1)return this;var n=k(arguments,1);return n.unshift(this),"function"==typeof e.install?e.install.apply(e,n):"function"==typeof e&&e.apply(null,n),t.push(e),this}}(e),function(e){e.mixin=function(e){return this.options=De(this.options,e),this}}(e),Cn(e),function(e){M.forEach(function(t){e[t]=function(e,n){return n?("component"===t&&s(n)&&(n.name=n.name||e,n=this.options._base.extend(n)),"directive"===t&&"function"==typeof n&&(n={bind:n,update:n}),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}})}(e)}(wn),Object.defineProperty(wn.prototype,"$isServer",{get:te}),Object.defineProperty(wn.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(wn,"FunctionalRenderContext",{value:Tt}),wn.version="2.6.11";var En=p("style,class"),Nn=p("input,textarea,option,select,progress"),jn=function(e,t,n){return"value"===n&&Nn(e)&&"button"!==t||"selected"===n&&"option"===e||"checked"===n&&"input"===e||"muted"===n&&"video"===e},Dn=p("contenteditable,draggable,spellcheck"),Ln=p("events,caret,typing,plaintext-only"),Mn=function(e,t){return Hn(t)||"false"===t?"false":"contenteditable"===e&&Ln(t)?t:"true"},In=p("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"),Fn="http://www.w3.org/1999/xlink",Pn=function(e){return":"===e.charAt(5)&&"xlink"===e.slice(0,5)},Rn=function(e){return Pn(e)?e.slice(6,e.length):""},Hn=function(e){return null==e||!1===e};function Bn(e){for(var t=e.data,r=e,i=e;n(i.componentInstance);)(i=i.componentInstance._vnode)&&i.data&&(t=Un(i.data,t));for(;n(r=r.parent);)r&&r.data&&(t=Un(t,r.data));return function(e,t){if(n(e)||n(t))return zn(e,Vn(t));return""}(t.staticClass,t.class)}function Un(e,t){return{staticClass:zn(e.staticClass,t.staticClass),class:n(e.class)?[e.class,t.class]:t.class}}function zn(e,t){return e?t?e+" "+t:e:t||""}function Vn(e){return Array.isArray(e)?function(e){for(var t,r="",i=0,o=e.length;i-1?hr(e,t,n):In(t)?Hn(n)?e.removeAttribute(t):(n="allowfullscreen"===t&&"EMBED"===e.tagName?"true":t,e.setAttribute(t,n)):Dn(t)?e.setAttribute(t,Mn(t,n)):Pn(t)?Hn(n)?e.removeAttributeNS(Fn,Rn(t)):e.setAttributeNS(Fn,t,n):hr(e,t,n)}function hr(e,t,n){if(Hn(n))e.removeAttribute(t);else{if(q&&!W&&"TEXTAREA"===e.tagName&&"placeholder"===t&&""!==n&&!e.__ieph){var r=function(t){t.stopImmediatePropagation(),e.removeEventListener("input",r)};e.addEventListener("input",r),e.__ieph=!0}e.setAttribute(t,n)}}var mr={create:dr,update:dr};function yr(e,r){var i=r.elm,o=r.data,a=e.data;if(!(t(o.staticClass)&&t(o.class)&&(t(a)||t(a.staticClass)&&t(a.class)))){var s=Bn(r),c=i._transitionClasses;n(c)&&(s=zn(s,Vn(c))),s!==i._prevClass&&(i.setAttribute("class",s),i._prevClass=s)}}var gr,_r,br,$r,wr,Cr,xr={create:yr,update:yr},kr=/[\w).+\-_$\]]/;function Ar(e){var t,n,r,i,o,a=!1,s=!1,c=!1,u=!1,l=0,f=0,p=0,d=0;for(r=0;r=0&&" "===(h=e.charAt(v));v--);h&&kr.test(h)||(u=!0)}}else void 0===i?(d=r+1,i=e.slice(0,r).trim()):m();function m(){(o||(o=[])).push(e.slice(d,r).trim()),d=r+1}if(void 0===i?i=e.slice(0,r).trim():0!==d&&m(),o)for(r=0;r-1?{exp:e.slice(0,$r),key:'"'+e.slice($r+1)+'"'}:{exp:e,key:null};_r=e,$r=wr=Cr=0;for(;!zr();)Vr(br=Ur())?Jr(br):91===br&&Kr(br);return{exp:e.slice(0,wr),key:e.slice(wr+1,Cr)}}(e);return null===n.key?e+"="+t:"$set("+n.exp+", "+n.key+", "+t+")"}function Ur(){return _r.charCodeAt(++$r)}function zr(){return $r>=gr}function Vr(e){return 34===e||39===e}function Kr(e){var t=1;for(wr=$r;!zr();)if(Vr(e=Ur()))Jr(e);else if(91===e&&t++,93===e&&t--,0===t){Cr=$r;break}}function Jr(e){for(var t=e;!zr()&&(e=Ur())!==t;);}var qr,Wr="__r",Zr="__c";function Gr(e,t,n){var r=qr;return function i(){null!==t.apply(null,arguments)&&Qr(e,i,n,r)}}var Xr=Ve&&!(X&&Number(X[1])<=53);function Yr(e,t,n,r){if(Xr){var i=an,o=t;t=o._wrapper=function(e){if(e.target===e.currentTarget||e.timeStamp>=i||e.timeStamp<=0||e.target.ownerDocument!==document)return o.apply(this,arguments)}}qr.addEventListener(e,t,Q?{capture:n,passive:r}:n)}function Qr(e,t,n,r){(r||qr).removeEventListener(e,t._wrapper||t,n)}function ei(e,r){if(!t(e.data.on)||!t(r.data.on)){var i=r.data.on||{},o=e.data.on||{};qr=r.elm,function(e){if(n(e[Wr])){var t=q?"change":"input";e[t]=[].concat(e[Wr],e[t]||[]),delete e[Wr]}n(e[Zr])&&(e.change=[].concat(e[Zr],e.change||[]),delete e[Zr])}(i),rt(i,o,Yr,Qr,Gr,r.context),qr=void 0}}var ti,ni={create:ei,update:ei};function ri(e,r){if(!t(e.data.domProps)||!t(r.data.domProps)){var i,o,a=r.elm,s=e.data.domProps||{},c=r.data.domProps||{};for(i in n(c.__ob__)&&(c=r.data.domProps=A({},c)),s)i in c||(a[i]="");for(i in c){if(o=c[i],"textContent"===i||"innerHTML"===i){if(r.children&&(r.children.length=0),o===s[i])continue;1===a.childNodes.length&&a.removeChild(a.childNodes[0])}if("value"===i&&"PROGRESS"!==a.tagName){a._value=o;var u=t(o)?"":String(o);ii(a,u)&&(a.value=u)}else if("innerHTML"===i&&qn(a.tagName)&&t(a.innerHTML)){(ti=ti||document.createElement("div")).innerHTML=""+o+"";for(var l=ti.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;l.firstChild;)a.appendChild(l.firstChild)}else if(o!==s[i])try{a[i]=o}catch(e){}}}}function ii(e,t){return!e.composing&&("OPTION"===e.tagName||function(e,t){var n=!0;try{n=document.activeElement!==e}catch(e){}return n&&e.value!==t}(e,t)||function(e,t){var r=e.value,i=e._vModifiers;if(n(i)){if(i.number)return f(r)!==f(t);if(i.trim)return r.trim()!==t.trim()}return r!==t}(e,t))}var oi={create:ri,update:ri},ai=g(function(e){var t={},n=/:(.+)/;return e.split(/;(?![^(]*\))/g).forEach(function(e){if(e){var r=e.split(n);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t});function si(e){var t=ci(e.style);return e.staticStyle?A(e.staticStyle,t):t}function ci(e){return Array.isArray(e)?O(e):"string"==typeof e?ai(e):e}var ui,li=/^--/,fi=/\s*!important$/,pi=function(e,t,n){if(li.test(t))e.style.setProperty(t,n);else if(fi.test(n))e.style.setProperty(C(t),n.replace(fi,""),"important");else{var r=vi(t);if(Array.isArray(n))for(var i=0,o=n.length;i-1?t.split(yi).forEach(function(t){return e.classList.add(t)}):e.classList.add(t);else{var n=" "+(e.getAttribute("class")||"")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function _i(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(yi).forEach(function(t){return e.classList.remove(t)}):e.classList.remove(t),e.classList.length||e.removeAttribute("class");else{for(var n=" "+(e.getAttribute("class")||"")+" ",r=" "+t+" ";n.indexOf(r)>=0;)n=n.replace(r," ");(n=n.trim())?e.setAttribute("class",n):e.removeAttribute("class")}}function bi(e){if(e){if("object"==typeof e){var t={};return!1!==e.css&&A(t,$i(e.name||"v")),A(t,e),t}return"string"==typeof e?$i(e):void 0}}var $i=g(function(e){return{enterClass:e+"-enter",enterToClass:e+"-enter-to",enterActiveClass:e+"-enter-active",leaveClass:e+"-leave",leaveToClass:e+"-leave-to",leaveActiveClass:e+"-leave-active"}}),wi=z&&!W,Ci="transition",xi="animation",ki="transition",Ai="transitionend",Oi="animation",Si="animationend";wi&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(ki="WebkitTransition",Ai="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(Oi="WebkitAnimation",Si="webkitAnimationEnd"));var Ti=z?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(e){return e()};function Ei(e){Ti(function(){Ti(e)})}function Ni(e,t){var n=e._transitionClasses||(e._transitionClasses=[]);n.indexOf(t)<0&&(n.push(t),gi(e,t))}function ji(e,t){e._transitionClasses&&h(e._transitionClasses,t),_i(e,t)}function Di(e,t,n){var r=Mi(e,t),i=r.type,o=r.timeout,a=r.propCount;if(!i)return n();var s=i===Ci?Ai:Si,c=0,u=function(){e.removeEventListener(s,l),n()},l=function(t){t.target===e&&++c>=a&&u()};setTimeout(function(){c0&&(n=Ci,l=a,f=o.length):t===xi?u>0&&(n=xi,l=u,f=c.length):f=(n=(l=Math.max(a,u))>0?a>u?Ci:xi:null)?n===Ci?o.length:c.length:0,{type:n,timeout:l,propCount:f,hasTransform:n===Ci&&Li.test(r[ki+"Property"])}}function Ii(e,t){for(;e.length1}function Ui(e,t){!0!==t.data.show&&Pi(t)}var zi=function(e){var o,a,s={},c=e.modules,u=e.nodeOps;for(o=0;ov?_(e,t(i[y+1])?null:i[y+1].elm,i,d,y,o):d>y&&$(r,p,v)}(p,h,y,o,l):n(y)?(n(e.text)&&u.setTextContent(p,""),_(p,null,y,0,y.length-1,o)):n(h)?$(h,0,h.length-1):n(e.text)&&u.setTextContent(p,""):e.text!==i.text&&u.setTextContent(p,i.text),n(v)&&n(d=v.hook)&&n(d=d.postpatch)&&d(e,i)}}}function k(e,t,i){if(r(i)&&n(e.parent))e.parent.data.pendingInsert=t;else for(var o=0;o-1,a.selected!==o&&(a.selected=o);else if(N(Wi(a),r))return void(e.selectedIndex!==s&&(e.selectedIndex=s));i||(e.selectedIndex=-1)}}function qi(e,t){return t.every(function(t){return!N(t,e)})}function Wi(e){return"_value"in e?e._value:e.value}function Zi(e){e.target.composing=!0}function Gi(e){e.target.composing&&(e.target.composing=!1,Xi(e.target,"input"))}function Xi(e,t){var n=document.createEvent("HTMLEvents");n.initEvent(t,!0,!0),e.dispatchEvent(n)}function Yi(e){return!e.componentInstance||e.data&&e.data.transition?e:Yi(e.componentInstance._vnode)}var Qi={model:Vi,show:{bind:function(e,t,n){var r=t.value,i=(n=Yi(n)).data&&n.data.transition,o=e.__vOriginalDisplay="none"===e.style.display?"":e.style.display;r&&i?(n.data.show=!0,Pi(n,function(){e.style.display=o})):e.style.display=r?o:"none"},update:function(e,t,n){var r=t.value;!r!=!t.oldValue&&((n=Yi(n)).data&&n.data.transition?(n.data.show=!0,r?Pi(n,function(){e.style.display=e.__vOriginalDisplay}):Ri(n,function(){e.style.display="none"})):e.style.display=r?e.__vOriginalDisplay:"none")},unbind:function(e,t,n,r,i){i||(e.style.display=e.__vOriginalDisplay)}}},eo={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function to(e){var t=e&&e.componentOptions;return t&&t.Ctor.options.abstract?to(zt(t.children)):e}function no(e){var t={},n=e.$options;for(var r in n.propsData)t[r]=e[r];var i=n._parentListeners;for(var o in i)t[b(o)]=i[o];return t}function ro(e,t){if(/\d-keep-alive$/.test(t.tag))return e("keep-alive",{props:t.componentOptions.propsData})}var io=function(e){return e.tag||Ut(e)},oo=function(e){return"show"===e.name},ao={name:"transition",props:eo,abstract:!0,render:function(e){var t=this,n=this.$slots.default;if(n&&(n=n.filter(io)).length){var r=this.mode,o=n[0];if(function(e){for(;e=e.parent;)if(e.data.transition)return!0}(this.$vnode))return o;var a=to(o);if(!a)return o;if(this._leaving)return ro(e,o);var s="__transition-"+this._uid+"-";a.key=null==a.key?a.isComment?s+"comment":s+a.tag:i(a.key)?0===String(a.key).indexOf(s)?a.key:s+a.key:a.key;var c=(a.data||(a.data={})).transition=no(this),u=this._vnode,l=to(u);if(a.data.directives&&a.data.directives.some(oo)&&(a.data.show=!0),l&&l.data&&!function(e,t){return t.key===e.key&&t.tag===e.tag}(a,l)&&!Ut(l)&&(!l.componentInstance||!l.componentInstance._vnode.isComment)){var f=l.data.transition=A({},c);if("out-in"===r)return this._leaving=!0,it(f,"afterLeave",function(){t._leaving=!1,t.$forceUpdate()}),ro(e,o);if("in-out"===r){if(Ut(a))return u;var p,d=function(){p()};it(c,"afterEnter",d),it(c,"enterCancelled",d),it(f,"delayLeave",function(e){p=e})}}return o}}},so=A({tag:String,moveClass:String},eo);function co(e){e.elm._moveCb&&e.elm._moveCb(),e.elm._enterCb&&e.elm._enterCb()}function uo(e){e.data.newPos=e.elm.getBoundingClientRect()}function lo(e){var t=e.data.pos,n=e.data.newPos,r=t.left-n.left,i=t.top-n.top;if(r||i){e.data.moved=!0;var o=e.elm.style;o.transform=o.WebkitTransform="translate("+r+"px,"+i+"px)",o.transitionDuration="0s"}}delete so.mode;var fo={Transition:ao,TransitionGroup:{props:so,beforeMount:function(){var e=this,t=this._update;this._update=function(n,r){var i=Zt(e);e.__patch__(e._vnode,e.kept,!1,!0),e._vnode=e.kept,i(),t.call(e,n,r)}},render:function(e){for(var t=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,i=this.$slots.default||[],o=this.children=[],a=no(this),s=0;s-1?Gn[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:Gn[e]=/HTMLUnknownElement/.test(t.toString())},A(wn.options.directives,Qi),A(wn.options.components,fo),wn.prototype.__patch__=z?zi:S,wn.prototype.$mount=function(e,t){return function(e,t,n){var r;return e.$el=t,e.$options.render||(e.$options.render=ve),Yt(e,"beforeMount"),r=function(){e._update(e._render(),n)},new fn(e,r,S,{before:function(){e._isMounted&&!e._isDestroyed&&Yt(e,"beforeUpdate")}},!0),n=!1,null==e.$vnode&&(e._isMounted=!0,Yt(e,"mounted")),e}(this,e=e&&z?Yn(e):void 0,t)},z&&setTimeout(function(){F.devtools&&ne&&ne.emit("init",wn)},0);var po=/\{\{((?:.|\r?\n)+?)\}\}/g,vo=/[-.*+?^${}()|[\]\/\\]/g,ho=g(function(e){var t=e[0].replace(vo,"\\$&"),n=e[1].replace(vo,"\\$&");return new RegExp(t+"((?:.|\\n)+?)"+n,"g")});var mo={staticKeys:["staticClass"],transformNode:function(e,t){t.warn;var n=Fr(e,"class");n&&(e.staticClass=JSON.stringify(n));var r=Ir(e,"class",!1);r&&(e.classBinding=r)},genData:function(e){var t="";return e.staticClass&&(t+="staticClass:"+e.staticClass+","),e.classBinding&&(t+="class:"+e.classBinding+","),t}};var yo,go={staticKeys:["staticStyle"],transformNode:function(e,t){t.warn;var n=Fr(e,"style");n&&(e.staticStyle=JSON.stringify(ai(n)));var r=Ir(e,"style",!1);r&&(e.styleBinding=r)},genData:function(e){var t="";return e.staticStyle&&(t+="staticStyle:"+e.staticStyle+","),e.styleBinding&&(t+="style:("+e.styleBinding+"),"),t}},_o=function(e){return(yo=yo||document.createElement("div")).innerHTML=e,yo.textContent},bo=p("area,base,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr"),$o=p("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source"),wo=p("address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track"),Co=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,xo=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,ko="[a-zA-Z_][\\-\\.0-9_a-zA-Z"+P.source+"]*",Ao="((?:"+ko+"\\:)?"+ko+")",Oo=new RegExp("^<"+Ao),So=/^\s*(\/?)>/,To=new RegExp("^<\\/"+Ao+"[^>]*>"),Eo=/^]+>/i,No=/^",""":'"',"&":"&"," ":"\n"," ":"\t","'":"'"},Io=/&(?:lt|gt|quot|amp|#39);/g,Fo=/&(?:lt|gt|quot|amp|#39|#10|#9);/g,Po=p("pre,textarea",!0),Ro=function(e,t){return e&&Po(e)&&"\n"===t[0]};function Ho(e,t){var n=t?Fo:Io;return e.replace(n,function(e){return Mo[e]})}var Bo,Uo,zo,Vo,Ko,Jo,qo,Wo,Zo=/^@|^v-on:/,Go=/^v-|^@|^:|^#/,Xo=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,Yo=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,Qo=/^\(|\)$/g,ea=/^\[.*\]$/,ta=/:(.*)$/,na=/^:|^\.|^v-bind:/,ra=/\.[^.\]]+(?=[^\]]*$)/g,ia=/^v-slot(:|$)|^#/,oa=/[\r\n]/,aa=/\s+/g,sa=g(_o),ca="_empty_";function ua(e,t,n){return{type:1,tag:e,attrsList:t,attrsMap:ma(t),rawAttrsMap:{},parent:n,children:[]}}function la(e,t){Bo=t.warn||Sr,Jo=t.isPreTag||T,qo=t.mustUseProp||T,Wo=t.getTagNamespace||T;t.isReservedTag;zo=Tr(t.modules,"transformNode"),Vo=Tr(t.modules,"preTransformNode"),Ko=Tr(t.modules,"postTransformNode"),Uo=t.delimiters;var n,r,i=[],o=!1!==t.preserveWhitespace,a=t.whitespace,s=!1,c=!1;function u(e){if(l(e),s||e.processed||(e=fa(e,t)),i.length||e===n||n.if&&(e.elseif||e.else)&&da(n,{exp:e.elseif,block:e}),r&&!e.forbidden)if(e.elseif||e.else)a=e,(u=function(e){var t=e.length;for(;t--;){if(1===e[t].type)return e[t];e.pop()}}(r.children))&&u.if&&da(u,{exp:a.elseif,block:a});else{if(e.slotScope){var o=e.slotTarget||'"default"';(r.scopedSlots||(r.scopedSlots={}))[o]=e}r.children.push(e),e.parent=r}var a,u;e.children=e.children.filter(function(e){return!e.slotScope}),l(e),e.pre&&(s=!1),Jo(e.tag)&&(c=!1);for(var f=0;f]*>)","i")),p=e.replace(f,function(e,n,r){return u=r.length,Do(l)||"noscript"===l||(n=n.replace(//g,"$1").replace(//g,"$1")),Ro(l,n)&&(n=n.slice(1)),t.chars&&t.chars(n),""});c+=e.length-p.length,e=p,A(l,c-u,c)}else{var d=e.indexOf("<");if(0===d){if(No.test(e)){var v=e.indexOf("--\x3e");if(v>=0){t.shouldKeepComment&&t.comment(e.substring(4,v),c,c+v+3),C(v+3);continue}}if(jo.test(e)){var h=e.indexOf("]>");if(h>=0){C(h+2);continue}}var m=e.match(Eo);if(m){C(m[0].length);continue}var y=e.match(To);if(y){var g=c;C(y[0].length),A(y[1],g,c);continue}var _=x();if(_){k(_),Ro(_.tagName,e)&&C(1);continue}}var b=void 0,$=void 0,w=void 0;if(d>=0){for($=e.slice(d);!(To.test($)||Oo.test($)||No.test($)||jo.test($)||(w=$.indexOf("<",1))<0);)d+=w,$=e.slice(d);b=e.substring(0,d)}d<0&&(b=e),b&&C(b.length),t.chars&&b&&t.chars(b,c-b.length,c)}if(e===n){t.chars&&t.chars(e);break}}function C(t){c+=t,e=e.substring(t)}function x(){var t=e.match(Oo);if(t){var n,r,i={tagName:t[1],attrs:[],start:c};for(C(t[0].length);!(n=e.match(So))&&(r=e.match(xo)||e.match(Co));)r.start=c,C(r[0].length),r.end=c,i.attrs.push(r);if(n)return i.unarySlash=n[1],C(n[0].length),i.end=c,i}}function k(e){var n=e.tagName,c=e.unarySlash;o&&("p"===r&&wo(n)&&A(r),s(n)&&r===n&&A(n));for(var u=a(n)||!!c,l=e.attrs.length,f=new Array(l),p=0;p=0&&i[a].lowerCasedTag!==s;a--);else a=0;if(a>=0){for(var u=i.length-1;u>=a;u--)t.end&&t.end(i[u].tag,n,o);i.length=a,r=a&&i[a-1].tag}else"br"===s?t.start&&t.start(e,[],!0,n,o):"p"===s&&(t.start&&t.start(e,[],!1,n,o),t.end&&t.end(e,n,o))}A()}(e,{warn:Bo,expectHTML:t.expectHTML,isUnaryTag:t.isUnaryTag,canBeLeftOpenTag:t.canBeLeftOpenTag,shouldDecodeNewlines:t.shouldDecodeNewlines,shouldDecodeNewlinesForHref:t.shouldDecodeNewlinesForHref,shouldKeepComment:t.comments,outputSourceRange:t.outputSourceRange,start:function(e,o,a,l,f){var p=r&&r.ns||Wo(e);q&&"svg"===p&&(o=function(e){for(var t=[],n=0;nc&&(s.push(o=e.slice(c,i)),a.push(JSON.stringify(o)));var u=Ar(r[1].trim());a.push("_s("+u+")"),s.push({"@binding":u}),c=i+r[0].length}return c-1"+("true"===o?":("+t+")":":_q("+t+","+o+")")),Mr(e,"change","var $$a="+t+",$$el=$event.target,$$c=$$el.checked?("+o+"):("+a+");if(Array.isArray($$a)){var $$v="+(r?"_n("+i+")":i)+",$$i=_i($$a,$$v);if($$el.checked){$$i<0&&("+Br(t,"$$a.concat([$$v])")+")}else{$$i>-1&&("+Br(t,"$$a.slice(0,$$i).concat($$a.slice($$i+1))")+")}}else{"+Br(t,"$$c")+"}",null,!0)}(e,r,i);else if("input"===o&&"radio"===a)!function(e,t,n){var r=n&&n.number,i=Ir(e,"value")||"null";Er(e,"checked","_q("+t+","+(i=r?"_n("+i+")":i)+")"),Mr(e,"change",Br(t,i),null,!0)}(e,r,i);else if("input"===o||"textarea"===o)!function(e,t,n){var r=e.attrsMap.type,i=n||{},o=i.lazy,a=i.number,s=i.trim,c=!o&&"range"!==r,u=o?"change":"range"===r?Wr:"input",l="$event.target.value";s&&(l="$event.target.value.trim()"),a&&(l="_n("+l+")");var f=Br(t,l);c&&(f="if($event.target.composing)return;"+f),Er(e,"value","("+t+")"),Mr(e,u,f,null,!0),(s||a)&&Mr(e,"blur","$forceUpdate()")}(e,r,i);else if(!F.isReservedTag(o))return Hr(e,r,i),!1;return!0},text:function(e,t){t.value&&Er(e,"textContent","_s("+t.value+")",t)},html:function(e,t){t.value&&Er(e,"innerHTML","_s("+t.value+")",t)}},isPreTag:function(e){return"pre"===e},isUnaryTag:bo,mustUseProp:jn,canBeLeftOpenTag:$o,isReservedTag:Wn,getTagNamespace:Zn,staticKeys:function(e){return e.reduce(function(e,t){return e.concat(t.staticKeys||[])},[]).join(",")}(ba)},xa=g(function(e){return p("type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap"+(e?","+e:""))});function ka(e,t){e&&($a=xa(t.staticKeys||""),wa=t.isReservedTag||T,function e(t){t.static=function(e){if(2===e.type)return!1;if(3===e.type)return!0;return!(!e.pre&&(e.hasBindings||e.if||e.for||d(e.tag)||!wa(e.tag)||function(e){for(;e.parent;){if("template"!==(e=e.parent).tag)return!1;if(e.for)return!0}return!1}(e)||!Object.keys(e).every($a)))}(t);if(1===t.type){if(!wa(t.tag)&&"slot"!==t.tag&&null==t.attrsMap["inline-template"])return;for(var n=0,r=t.children.length;n|^function(?:\s+[\w$]+)?\s*\(/,Oa=/\([^)]*?\);*$/,Sa=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,Ta={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},Ea={esc:["Esc","Escape"],tab:"Tab",enter:"Enter",space:[" ","Spacebar"],up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete","Del"]},Na=function(e){return"if("+e+")return null;"},ja={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:Na("$event.target !== $event.currentTarget"),ctrl:Na("!$event.ctrlKey"),shift:Na("!$event.shiftKey"),alt:Na("!$event.altKey"),meta:Na("!$event.metaKey"),left:Na("'button' in $event && $event.button !== 0"),middle:Na("'button' in $event && $event.button !== 1"),right:Na("'button' in $event && $event.button !== 2")};function Da(e,t){var n=t?"nativeOn:":"on:",r="",i="";for(var o in e){var a=La(e[o]);e[o]&&e[o].dynamic?i+=o+","+a+",":r+='"'+o+'":'+a+","}return r="{"+r.slice(0,-1)+"}",i?n+"_d("+r+",["+i.slice(0,-1)+"])":n+r}function La(e){if(!e)return"function(){}";if(Array.isArray(e))return"["+e.map(function(e){return La(e)}).join(",")+"]";var t=Sa.test(e.value),n=Aa.test(e.value),r=Sa.test(e.value.replace(Oa,""));if(e.modifiers){var i="",o="",a=[];for(var s in e.modifiers)if(ja[s])o+=ja[s],Ta[s]&&a.push(s);else if("exact"===s){var c=e.modifiers;o+=Na(["ctrl","shift","alt","meta"].filter(function(e){return!c[e]}).map(function(e){return"$event."+e+"Key"}).join("||"))}else a.push(s);return a.length&&(i+=function(e){return"if(!$event.type.indexOf('key')&&"+e.map(Ma).join("&&")+")return null;"}(a)),o&&(i+=o),"function($event){"+i+(t?"return "+e.value+"($event)":n?"return ("+e.value+")($event)":r?"return "+e.value:e.value)+"}"}return t||n?e.value:"function($event){"+(r?"return "+e.value:e.value)+"}"}function Ma(e){var t=parseInt(e,10);if(t)return"$event.keyCode!=="+t;var n=Ta[e],r=Ea[e];return"_k($event.keyCode,"+JSON.stringify(e)+","+JSON.stringify(n)+",$event.key,"+JSON.stringify(r)+")"}var Ia={on:function(e,t){e.wrapListeners=function(e){return"_g("+e+","+t.value+")"}},bind:function(e,t){e.wrapData=function(n){return"_b("+n+",'"+e.tag+"',"+t.value+","+(t.modifiers&&t.modifiers.prop?"true":"false")+(t.modifiers&&t.modifiers.sync?",true":"")+")"}},cloak:S},Fa=function(e){this.options=e,this.warn=e.warn||Sr,this.transforms=Tr(e.modules,"transformCode"),this.dataGenFns=Tr(e.modules,"genData"),this.directives=A(A({},Ia),e.directives);var t=e.isReservedTag||T;this.maybeComponent=function(e){return!!e.component||!t(e.tag)},this.onceId=0,this.staticRenderFns=[],this.pre=!1};function Pa(e,t){var n=new Fa(t);return{render:"with(this){return "+(e?Ra(e,n):'_c("div")')+"}",staticRenderFns:n.staticRenderFns}}function Ra(e,t){if(e.parent&&(e.pre=e.pre||e.parent.pre),e.staticRoot&&!e.staticProcessed)return Ha(e,t);if(e.once&&!e.onceProcessed)return Ba(e,t);if(e.for&&!e.forProcessed)return za(e,t);if(e.if&&!e.ifProcessed)return Ua(e,t);if("template"!==e.tag||e.slotTarget||t.pre){if("slot"===e.tag)return function(e,t){var n=e.slotName||'"default"',r=qa(e,t),i="_t("+n+(r?","+r:""),o=e.attrs||e.dynamicAttrs?Ga((e.attrs||[]).concat(e.dynamicAttrs||[]).map(function(e){return{name:b(e.name),value:e.value,dynamic:e.dynamic}})):null,a=e.attrsMap["v-bind"];!o&&!a||r||(i+=",null");o&&(i+=","+o);a&&(i+=(o?"":",null")+","+a);return i+")"}(e,t);var n;if(e.component)n=function(e,t,n){var r=t.inlineTemplate?null:qa(t,n,!0);return"_c("+e+","+Va(t,n)+(r?","+r:"")+")"}(e.component,e,t);else{var r;(!e.plain||e.pre&&t.maybeComponent(e))&&(r=Va(e,t));var i=e.inlineTemplate?null:qa(e,t,!0);n="_c('"+e.tag+"'"+(r?","+r:"")+(i?","+i:"")+")"}for(var o=0;o>>0}(a):"")+")"}(e,e.scopedSlots,t)+","),e.model&&(n+="model:{value:"+e.model.value+",callback:"+e.model.callback+",expression:"+e.model.expression+"},"),e.inlineTemplate){var o=function(e,t){var n=e.children[0];if(n&&1===n.type){var r=Pa(n,t.options);return"inlineTemplate:{render:function(){"+r.render+"},staticRenderFns:["+r.staticRenderFns.map(function(e){return"function(){"+e+"}"}).join(",")+"]}"}}(e,t);o&&(n+=o+",")}return n=n.replace(/,$/,"")+"}",e.dynamicAttrs&&(n="_b("+n+',"'+e.tag+'",'+Ga(e.dynamicAttrs)+")"),e.wrapData&&(n=e.wrapData(n)),e.wrapListeners&&(n=e.wrapListeners(n)),n}function Ka(e){return 1===e.type&&("slot"===e.tag||e.children.some(Ka))}function Ja(e,t){var n=e.attrsMap["slot-scope"];if(e.if&&!e.ifProcessed&&!n)return Ua(e,t,Ja,"null");if(e.for&&!e.forProcessed)return za(e,t,Ja);var r=e.slotScope===ca?"":String(e.slotScope),i="function("+r+"){return "+("template"===e.tag?e.if&&n?"("+e.if+")?"+(qa(e,t)||"undefined")+":undefined":qa(e,t)||"undefined":Ra(e,t))+"}",o=r?"":",proxy:true";return"{key:"+(e.slotTarget||'"default"')+",fn:"+i+o+"}"}function qa(e,t,n,r,i){var o=e.children;if(o.length){var a=o[0];if(1===o.length&&a.for&&"template"!==a.tag&&"slot"!==a.tag){var s=n?t.maybeComponent(a)?",1":",0":"";return""+(r||Ra)(a,t)+s}var c=n?function(e,t){for(var n=0,r=0;r':'
    ',ts.innerHTML.indexOf(" ")>0}var os=!!z&&is(!1),as=!!z&&is(!0),ss=g(function(e){var t=Yn(e);return t&&t.innerHTML}),cs=wn.prototype.$mount;return wn.prototype.$mount=function(e,t){if((e=e&&Yn(e))===document.body||e===document.documentElement)return this;var n=this.$options;if(!n.render){var r=n.template;if(r)if("string"==typeof r)"#"===r.charAt(0)&&(r=ss(r));else{if(!r.nodeType)return this;r=r.innerHTML}else e&&(r=function(e){if(e.outerHTML)return e.outerHTML;var t=document.createElement("div");return t.appendChild(e.cloneNode(!0)),t.innerHTML}(e));if(r){var i=rs(r,{outputSourceRange:!1,shouldDecodeNewlines:os,shouldDecodeNewlinesForHref:as,delimiters:n.delimiters,comments:n.comments},this),o=i.render,a=i.staticRenderFns;n.render=o,n.staticRenderFns=a}}return cs.call(this,e,t)},wn.compile=rs,wn}); \ No newline at end of file diff --git a/html/js/external/vue-2.6.14.min.js b/html/js/external/vue-2.6.14.min.js new file mode 100644 index 00000000..d998ff72 --- /dev/null +++ b/html/js/external/vue-2.6.14.min.js @@ -0,0 +1,6 @@ +/*! + * Vue.js v2.6.14 + * (c) 2014-2021 Evan You + * Released under the MIT License. + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).Vue=t()}(this,function(){"use strict";var e=Object.freeze({});function t(e){return null==e}function n(e){return null!=e}function r(e){return!0===e}function i(e){return"string"==typeof e||"number"==typeof e||"symbol"==typeof e||"boolean"==typeof e}function o(e){return null!==e&&"object"==typeof e}var a=Object.prototype.toString;function s(e){return"[object Object]"===a.call(e)}function c(e){var t=parseFloat(String(e));return t>=0&&Math.floor(t)===t&&isFinite(e)}function u(e){return n(e)&&"function"==typeof e.then&&"function"==typeof e.catch}function l(e){return null==e?"":Array.isArray(e)||s(e)&&e.toString===a?JSON.stringify(e,null,2):String(e)}function f(e){var t=parseFloat(e);return isNaN(t)?e:t}function p(e,t){for(var n=Object.create(null),r=e.split(","),i=0;i-1)return e.splice(n,1)}}var m=Object.prototype.hasOwnProperty;function y(e,t){return m.call(e,t)}function g(e){var t=Object.create(null);return function(n){return t[n]||(t[n]=e(n))}}var _=/-(\w)/g,b=g(function(e){return e.replace(_,function(e,t){return t?t.toUpperCase():""})}),$=g(function(e){return e.charAt(0).toUpperCase()+e.slice(1)}),w=/\B([A-Z])/g,C=g(function(e){return e.replace(w,"-$1").toLowerCase()});var x=Function.prototype.bind?function(e,t){return e.bind(t)}:function(e,t){function n(n){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n};function k(e,t){t=t||0;for(var n=e.length-t,r=new Array(n);n--;)r[n]=e[n+t];return r}function A(e,t){for(var n in t)e[n]=t[n];return e}function O(e){for(var t={},n=0;n0,Z=J&&J.indexOf("edge/")>0,G=(J&&J.indexOf("android"),J&&/iphone|ipad|ipod|ios/.test(J)||"ios"===K),X=(J&&/chrome\/\d+/.test(J),J&&/phantomjs/.test(J),J&&J.match(/firefox\/(\d+)/)),Y={}.watch,Q=!1;if(V)try{var ee={};Object.defineProperty(ee,"passive",{get:function(){Q=!0}}),window.addEventListener("test-passive",null,ee)}catch(e){}var te=function(){return void 0===B&&(B=!V&&!z&&"undefined"!=typeof global&&(global.process&&"server"===global.process.env.VUE_ENV)),B},ne=V&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function re(e){return"function"==typeof e&&/native code/.test(e.toString())}var ie,oe="undefined"!=typeof Symbol&&re(Symbol)&&"undefined"!=typeof Reflect&&re(Reflect.ownKeys);ie="undefined"!=typeof Set&&re(Set)?Set:function(){function e(){this.set=Object.create(null)}return e.prototype.has=function(e){return!0===this.set[e]},e.prototype.add=function(e){this.set[e]=!0},e.prototype.clear=function(){this.set=Object.create(null)},e}();var ae=S,se=0,ce=function(){this.id=se++,this.subs=[]};ce.prototype.addSub=function(e){this.subs.push(e)},ce.prototype.removeSub=function(e){h(this.subs,e)},ce.prototype.depend=function(){ce.target&&ce.target.addDep(this)},ce.prototype.notify=function(){for(var e=this.subs.slice(),t=0,n=e.length;t-1)if(o&&!y(i,"default"))a=!1;else if(""===a||a===C(e)){var c=Re(String,i.type);(c<0||s0&&(ct((u=e(u,(a||"")+"_"+c))[0])&&ct(f)&&(s[l]=he(f.text+u[0].text),u.shift()),s.push.apply(s,u)):i(u)?ct(f)?s[l]=he(f.text+u):""!==u&&s.push(he(u)):ct(u)&&ct(f)?s[l]=he(f.text+u.text):(r(o._isVList)&&n(u.tag)&&t(u.key)&&n(a)&&(u.key="__vlist"+a+"_"+c+"__"),s.push(u)));return s}(e):void 0}function ct(e){return n(e)&&n(e.text)&&!1===e.isComment}function ut(e,t){if(e){for(var n=Object.create(null),r=oe?Reflect.ownKeys(e):Object.keys(e),i=0;i0,a=t?!!t.$stable:!o,s=t&&t.$key;if(t){if(t._normalized)return t._normalized;if(a&&r&&r!==e&&s===r.$key&&!o&&!r.$hasNormal)return r;for(var c in i={},t)t[c]&&"$"!==c[0]&&(i[c]=vt(n,c,t[c]))}else i={};for(var u in n)u in i||(i[u]=ht(n,u));return t&&Object.isExtensible(t)&&(t._normalized=i),R(i,"$stable",a),R(i,"$key",s),R(i,"$hasNormal",o),i}function vt(e,t,n){var r=function(){var e=arguments.length?n.apply(null,arguments):n({}),t=(e=e&&"object"==typeof e&&!Array.isArray(e)?[e]:st(e))&&e[0];return e&&(!t||1===e.length&&t.isComment&&!pt(t))?void 0:e};return n.proxy&&Object.defineProperty(e,t,{get:r,enumerable:!0,configurable:!0}),r}function ht(e,t){return function(){return e[t]}}function mt(e,t){var r,i,a,s,c;if(Array.isArray(e)||"string"==typeof e)for(r=new Array(e.length),i=0,a=e.length;idocument.createEvent("Event").timeStamp&&(cn=function(){return un.now()})}function ln(){var e,t;for(sn=cn(),on=!0,en.sort(function(e,t){return e.id-t.id}),an=0;anan&&en[n].id>e.id;)n--;en.splice(n+1,0,e)}else en.push(e);rn||(rn=!0,Qe(ln))}}(this)},pn.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||o(e)||this.deep){var t=this.value;if(this.value=e,this.user){var n='callback for watcher "'+this.expression+'"';Be(this.cb,this.vm,[e,t],this.vm,n)}else this.cb.call(this.vm,e,t)}}},pn.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},pn.prototype.depend=function(){for(var e=this.deps.length;e--;)this.deps[e].depend()},pn.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||h(this.vm._watchers,this);for(var e=this.deps.length;e--;)this.deps[e].removeSub(this);this.active=!1}};var dn={enumerable:!0,configurable:!0,get:S,set:S};function vn(e,t,n){dn.get=function(){return this[t][n]},dn.set=function(e){this[t][n]=e},Object.defineProperty(e,n,dn)}function hn(e){e._watchers=[];var t=e.$options;t.props&&function(e,t){var n=e.$options.propsData||{},r=e._props={},i=e.$options._propKeys=[];e.$parent&&$e(!1);var o=function(o){i.push(o);var a=Ie(o,t,n,e);xe(r,o,a),o in e||vn(e,"_props",o)};for(var a in t)o(a);$e(!0)}(e,t.props),t.methods&&function(e,t){e.$options.props;for(var n in t)e[n]="function"!=typeof t[n]?S:x(t[n],e)}(e,t.methods),t.data?function(e){var t=e.$options.data;s(t=e._data="function"==typeof t?function(e,t){le();try{return e.call(t,t)}catch(e){return He(e,t,"data()"),{}}finally{fe()}}(t,e):t||{})||(t={});var n=Object.keys(t),r=e.$options.props,i=(e.$options.methods,n.length);for(;i--;){var o=n[i];r&&y(r,o)||(a=void 0,36!==(a=(o+"").charCodeAt(0))&&95!==a&&vn(e,"_data",o))}var a;Ce(t,!0)}(e):Ce(e._data={},!0),t.computed&&function(e,t){var n=e._computedWatchers=Object.create(null),r=te();for(var i in t){var o=t[i],a="function"==typeof o?o:o.get;r||(n[i]=new pn(e,a||S,S,mn)),i in e||yn(e,i,o)}}(e,t.computed),t.watch&&t.watch!==Y&&function(e,t){for(var n in t){var r=t[n];if(Array.isArray(r))for(var i=0;i-1:"string"==typeof e?e.split(",").indexOf(t)>-1:(n=e,"[object RegExp]"===a.call(n)&&e.test(t));var n}function On(e,t){var n=e.cache,r=e.keys,i=e._vnode;for(var o in n){var a=n[o];if(a){var s=a.name;s&&!t(s)&&Sn(n,o,r,i)}}}function Sn(e,t,n,r){var i=e[t];!i||r&&i.tag===r.tag||i.componentInstance.$destroy(),e[t]=null,h(n,t)}!function(t){t.prototype._init=function(t){var n=this;n._uid=$n++,n._isVue=!0,t&&t._isComponent?function(e,t){var n=e.$options=Object.create(e.constructor.options),r=t._parentVnode;n.parent=t.parent,n._parentVnode=r;var i=r.componentOptions;n.propsData=i.propsData,n._parentListeners=i.listeners,n._renderChildren=i.children,n._componentTag=i.tag,t.render&&(n.render=t.render,n.staticRenderFns=t.staticRenderFns)}(n,t):n.$options=De(wn(n.constructor),t||{},n),n._renderProxy=n,n._self=n,function(e){var t=e.$options,n=t.parent;if(n&&!t.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(e)}e.$parent=n,e.$root=n?n.$root:e,e.$children=[],e.$refs={},e._watcher=null,e._inactive=null,e._directInactive=!1,e._isMounted=!1,e._isDestroyed=!1,e._isBeingDestroyed=!1}(n),function(e){e._events=Object.create(null),e._hasHookEvent=!1;var t=e.$options._parentListeners;t&&Wt(e,t)}(n),function(t){t._vnode=null,t._staticTrees=null;var n=t.$options,r=t.$vnode=n._parentVnode,i=r&&r.context;t.$slots=lt(n._renderChildren,i),t.$scopedSlots=e,t._c=function(e,n,r,i){return Ht(t,e,n,r,i,!1)},t.$createElement=function(e,n,r,i){return Ht(t,e,n,r,i,!0)};var o=r&&r.data;xe(t,"$attrs",o&&o.attrs||e,null,!0),xe(t,"$listeners",n._parentListeners||e,null,!0)}(n),Qt(n,"beforeCreate"),function(e){var t=ut(e.$options.inject,e);t&&($e(!1),Object.keys(t).forEach(function(n){xe(e,n,t[n])}),$e(!0))}(n),hn(n),function(e){var t=e.$options.provide;t&&(e._provided="function"==typeof t?t.call(e):t)}(n),Qt(n,"created"),n.$options.el&&n.$mount(n.$options.el)}}(Cn),function(e){var t={get:function(){return this._data}},n={get:function(){return this._props}};Object.defineProperty(e.prototype,"$data",t),Object.defineProperty(e.prototype,"$props",n),e.prototype.$set=ke,e.prototype.$delete=Ae,e.prototype.$watch=function(e,t,n){if(s(t))return bn(this,e,t,n);(n=n||{}).user=!0;var r=new pn(this,e,t,n);if(n.immediate){var i='callback for immediate watcher "'+r.expression+'"';le(),Be(t,this,[r.value],this,i),fe()}return function(){r.teardown()}}}(Cn),function(e){var t=/^hook:/;e.prototype.$on=function(e,n){var r=this;if(Array.isArray(e))for(var i=0,o=e.length;i1?k(t):t;for(var n=k(arguments,1),r='event handler for "'+e+'"',i=0,o=t.length;iparseInt(this.max)&&Sn(e,t[0],t,this._vnode),this.vnodeToCache=null}}},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var e in this.cache)Sn(this.cache,e,this.keys)},mounted:function(){var e=this;this.cacheVNode(),this.$watch("include",function(t){On(e,function(e){return An(t,e)})}),this.$watch("exclude",function(t){On(e,function(e){return!An(t,e)})})},updated:function(){this.cacheVNode()},render:function(){var e=this.$slots.default,t=zt(e),n=t&&t.componentOptions;if(n){var r=kn(n),i=this.include,o=this.exclude;if(i&&(!r||!An(i,r))||o&&r&&An(o,r))return t;var a=this.cache,s=this.keys,c=null==t.key?n.Ctor.cid+(n.tag?"::"+n.tag:""):t.key;a[c]?(t.componentInstance=a[c].componentInstance,h(s,c),s.push(c)):(this.vnodeToCache=t,this.keyToCache=c),t.data.keepAlive=!0}return t||e&&e[0]}}};!function(e){var t={get:function(){return F}};Object.defineProperty(e,"config",t),e.util={warn:ae,extend:A,mergeOptions:De,defineReactive:xe},e.set=ke,e.delete=Ae,e.nextTick=Qe,e.observable=function(e){return Ce(e),e},e.options=Object.create(null),I.forEach(function(t){e.options[t+"s"]=Object.create(null)}),e.options._base=e,A(e.options.components,Nn),function(e){e.use=function(e){var t=this._installedPlugins||(this._installedPlugins=[]);if(t.indexOf(e)>-1)return this;var n=k(arguments,1);return n.unshift(this),"function"==typeof e.install?e.install.apply(e,n):"function"==typeof e&&e.apply(null,n),t.push(e),this}}(e),function(e){e.mixin=function(e){return this.options=De(this.options,e),this}}(e),xn(e),function(e){I.forEach(function(t){e[t]=function(e,n){return n?("component"===t&&s(n)&&(n.name=n.name||e,n=this.options._base.extend(n)),"directive"===t&&"function"==typeof n&&(n={bind:n,update:n}),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}})}(e)}(Cn),Object.defineProperty(Cn.prototype,"$isServer",{get:te}),Object.defineProperty(Cn.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(Cn,"FunctionalRenderContext",{value:Et}),Cn.version="2.6.14";var En=p("style,class"),jn=p("input,textarea,option,select,progress"),Dn=function(e,t,n){return"value"===n&&jn(e)&&"button"!==t||"selected"===n&&"option"===e||"checked"===n&&"input"===e||"muted"===n&&"video"===e},Ln=p("contenteditable,draggable,spellcheck"),In=p("events,caret,typing,plaintext-only"),Mn=function(e,t){return Bn(t)||"false"===t?"false":"contenteditable"===e&&In(t)?t:"true"},Fn=p("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,truespeed,typemustmatch,visible"),Pn="http://www.w3.org/1999/xlink",Rn=function(e){return":"===e.charAt(5)&&"xlink"===e.slice(0,5)},Hn=function(e){return Rn(e)?e.slice(6,e.length):""},Bn=function(e){return null==e||!1===e};function Un(e){for(var t=e.data,r=e,i=e;n(i.componentInstance);)(i=i.componentInstance._vnode)&&i.data&&(t=Vn(i.data,t));for(;n(r=r.parent);)r&&r.data&&(t=Vn(t,r.data));return function(e,t){if(n(e)||n(t))return zn(e,Kn(t));return""}(t.staticClass,t.class)}function Vn(e,t){return{staticClass:zn(e.staticClass,t.staticClass),class:n(e.class)?[e.class,t.class]:t.class}}function zn(e,t){return e?t?e+" "+t:e:t||""}function Kn(e){return Array.isArray(e)?function(e){for(var t,r="",i=0,o=e.length;i-1?mr(e,t,n):Fn(t)?Bn(n)?e.removeAttribute(t):(n="allowfullscreen"===t&&"EMBED"===e.tagName?"true":t,e.setAttribute(t,n)):Ln(t)?e.setAttribute(t,Mn(t,n)):Rn(t)?Bn(n)?e.removeAttributeNS(Pn,Hn(t)):e.setAttributeNS(Pn,t,n):mr(e,t,n)}function mr(e,t,n){if(Bn(n))e.removeAttribute(t);else{if(q&&!W&&"TEXTAREA"===e.tagName&&"placeholder"===t&&""!==n&&!e.__ieph){var r=function(t){t.stopImmediatePropagation(),e.removeEventListener("input",r)};e.addEventListener("input",r),e.__ieph=!0}e.setAttribute(t,n)}}var yr={create:vr,update:vr};function gr(e,r){var i=r.elm,o=r.data,a=e.data;if(!(t(o.staticClass)&&t(o.class)&&(t(a)||t(a.staticClass)&&t(a.class)))){var s=Un(r),c=i._transitionClasses;n(c)&&(s=zn(s,Kn(c))),s!==i._prevClass&&(i.setAttribute("class",s),i._prevClass=s)}}var _r,br,$r,wr,Cr,xr,kr={create:gr,update:gr},Ar=/[\w).+\-_$\]]/;function Or(e){var t,n,r,i,o,a=!1,s=!1,c=!1,u=!1,l=0,f=0,p=0,d=0;for(r=0;r=0&&" "===(h=e.charAt(v));v--);h&&Ar.test(h)||(u=!0)}}else void 0===i?(d=r+1,i=e.slice(0,r).trim()):m();function m(){(o||(o=[])).push(e.slice(d,r).trim()),d=r+1}if(void 0===i?i=e.slice(0,r).trim():0!==d&&m(),o)for(r=0;r-1?{exp:e.slice(0,wr),key:'"'+e.slice(wr+1)+'"'}:{exp:e,key:null};br=e,wr=Cr=xr=0;for(;!zr();)Kr($r=Vr())?qr($r):91===$r&&Jr($r);return{exp:e.slice(0,Cr),key:e.slice(Cr+1,xr)}}(e);return null===n.key?e+"="+t:"$set("+n.exp+", "+n.key+", "+t+")"}function Vr(){return br.charCodeAt(++wr)}function zr(){return wr>=_r}function Kr(e){return 34===e||39===e}function Jr(e){var t=1;for(Cr=wr;!zr();)if(Kr(e=Vr()))qr(e);else if(91===e&&t++,93===e&&t--,0===t){xr=wr;break}}function qr(e){for(var t=e;!zr()&&(e=Vr())!==t;);}var Wr,Zr="__r",Gr="__c";function Xr(e,t,n){var r=Wr;return function i(){null!==t.apply(null,arguments)&&ei(e,i,n,r)}}var Yr=Ke&&!(X&&Number(X[1])<=53);function Qr(e,t,n,r){if(Yr){var i=sn,o=t;t=o._wrapper=function(e){if(e.target===e.currentTarget||e.timeStamp>=i||e.timeStamp<=0||e.target.ownerDocument!==document)return o.apply(this,arguments)}}Wr.addEventListener(e,t,Q?{capture:n,passive:r}:n)}function ei(e,t,n,r){(r||Wr).removeEventListener(e,t._wrapper||t,n)}function ti(e,r){if(!t(e.data.on)||!t(r.data.on)){var i=r.data.on||{},o=e.data.on||{};Wr=r.elm,function(e){if(n(e[Zr])){var t=q?"change":"input";e[t]=[].concat(e[Zr],e[t]||[]),delete e[Zr]}n(e[Gr])&&(e.change=[].concat(e[Gr],e.change||[]),delete e[Gr])}(i),it(i,o,Qr,ei,Xr,r.context),Wr=void 0}}var ni,ri={create:ti,update:ti};function ii(e,r){if(!t(e.data.domProps)||!t(r.data.domProps)){var i,o,a=r.elm,s=e.data.domProps||{},c=r.data.domProps||{};for(i in n(c.__ob__)&&(c=r.data.domProps=A({},c)),s)i in c||(a[i]="");for(i in c){if(o=c[i],"textContent"===i||"innerHTML"===i){if(r.children&&(r.children.length=0),o===s[i])continue;1===a.childNodes.length&&a.removeChild(a.childNodes[0])}if("value"===i&&"PROGRESS"!==a.tagName){a._value=o;var u=t(o)?"":String(o);oi(a,u)&&(a.value=u)}else if("innerHTML"===i&&Wn(a.tagName)&&t(a.innerHTML)){(ni=ni||document.createElement("div")).innerHTML=""+o+"";for(var l=ni.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;l.firstChild;)a.appendChild(l.firstChild)}else if(o!==s[i])try{a[i]=o}catch(e){}}}}function oi(e,t){return!e.composing&&("OPTION"===e.tagName||function(e,t){var n=!0;try{n=document.activeElement!==e}catch(e){}return n&&e.value!==t}(e,t)||function(e,t){var r=e.value,i=e._vModifiers;if(n(i)){if(i.number)return f(r)!==f(t);if(i.trim)return r.trim()!==t.trim()}return r!==t}(e,t))}var ai={create:ii,update:ii},si=g(function(e){var t={},n=/:(.+)/;return e.split(/;(?![^(]*\))/g).forEach(function(e){if(e){var r=e.split(n);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t});function ci(e){var t=ui(e.style);return e.staticStyle?A(e.staticStyle,t):t}function ui(e){return Array.isArray(e)?O(e):"string"==typeof e?si(e):e}var li,fi=/^--/,pi=/\s*!important$/,di=function(e,t,n){if(fi.test(t))e.style.setProperty(t,n);else if(pi.test(n))e.style.setProperty(C(t),n.replace(pi,""),"important");else{var r=hi(t);if(Array.isArray(n))for(var i=0,o=n.length;i-1?t.split(gi).forEach(function(t){return e.classList.add(t)}):e.classList.add(t);else{var n=" "+(e.getAttribute("class")||"")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function bi(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(gi).forEach(function(t){return e.classList.remove(t)}):e.classList.remove(t),e.classList.length||e.removeAttribute("class");else{for(var n=" "+(e.getAttribute("class")||"")+" ",r=" "+t+" ";n.indexOf(r)>=0;)n=n.replace(r," ");(n=n.trim())?e.setAttribute("class",n):e.removeAttribute("class")}}function $i(e){if(e){if("object"==typeof e){var t={};return!1!==e.css&&A(t,wi(e.name||"v")),A(t,e),t}return"string"==typeof e?wi(e):void 0}}var wi=g(function(e){return{enterClass:e+"-enter",enterToClass:e+"-enter-to",enterActiveClass:e+"-enter-active",leaveClass:e+"-leave",leaveToClass:e+"-leave-to",leaveActiveClass:e+"-leave-active"}}),Ci=V&&!W,xi="transition",ki="animation",Ai="transition",Oi="transitionend",Si="animation",Ti="animationend";Ci&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(Ai="WebkitTransition",Oi="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(Si="WebkitAnimation",Ti="webkitAnimationEnd"));var Ni=V?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(e){return e()};function Ei(e){Ni(function(){Ni(e)})}function ji(e,t){var n=e._transitionClasses||(e._transitionClasses=[]);n.indexOf(t)<0&&(n.push(t),_i(e,t))}function Di(e,t){e._transitionClasses&&h(e._transitionClasses,t),bi(e,t)}function Li(e,t,n){var r=Mi(e,t),i=r.type,o=r.timeout,a=r.propCount;if(!i)return n();var s=i===xi?Oi:Ti,c=0,u=function(){e.removeEventListener(s,l),n()},l=function(t){t.target===e&&++c>=a&&u()};setTimeout(function(){c0&&(n=xi,l=a,f=o.length):t===ki?u>0&&(n=ki,l=u,f=c.length):f=(n=(l=Math.max(a,u))>0?a>u?xi:ki:null)?n===xi?o.length:c.length:0,{type:n,timeout:l,propCount:f,hasTransform:n===xi&&Ii.test(r[Ai+"Property"])}}function Fi(e,t){for(;e.length1}function Vi(e,t){!0!==t.data.show&&Ri(t)}var zi=function(e){var o,a,s={},c=e.modules,u=e.nodeOps;for(o=0;ov?_(e,t(i[y+1])?null:i[y+1].elm,i,d,y,o):d>y&&$(r,p,v)}(p,h,y,o,l):n(y)?(n(e.text)&&u.setTextContent(p,""),_(p,null,y,0,y.length-1,o)):n(h)?$(h,0,h.length-1):n(e.text)&&u.setTextContent(p,""):e.text!==i.text&&u.setTextContent(p,i.text),n(v)&&n(d=v.hook)&&n(d=d.postpatch)&&d(e,i)}}}function k(e,t,i){if(r(i)&&n(e.parent))e.parent.data.pendingInsert=t;else for(var o=0;o-1,a.selected!==o&&(a.selected=o);else if(E(Zi(a),r))return void(e.selectedIndex!==s&&(e.selectedIndex=s));i||(e.selectedIndex=-1)}}function Wi(e,t){return t.every(function(t){return!E(t,e)})}function Zi(e){return"_value"in e?e._value:e.value}function Gi(e){e.target.composing=!0}function Xi(e){e.target.composing&&(e.target.composing=!1,Yi(e.target,"input"))}function Yi(e,t){var n=document.createEvent("HTMLEvents");n.initEvent(t,!0,!0),e.dispatchEvent(n)}function Qi(e){return!e.componentInstance||e.data&&e.data.transition?e:Qi(e.componentInstance._vnode)}var eo={model:Ki,show:{bind:function(e,t,n){var r=t.value,i=(n=Qi(n)).data&&n.data.transition,o=e.__vOriginalDisplay="none"===e.style.display?"":e.style.display;r&&i?(n.data.show=!0,Ri(n,function(){e.style.display=o})):e.style.display=r?o:"none"},update:function(e,t,n){var r=t.value;!r!=!t.oldValue&&((n=Qi(n)).data&&n.data.transition?(n.data.show=!0,r?Ri(n,function(){e.style.display=e.__vOriginalDisplay}):Hi(n,function(){e.style.display="none"})):e.style.display=r?e.__vOriginalDisplay:"none")},unbind:function(e,t,n,r,i){i||(e.style.display=e.__vOriginalDisplay)}}},to={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function no(e){var t=e&&e.componentOptions;return t&&t.Ctor.options.abstract?no(zt(t.children)):e}function ro(e){var t={},n=e.$options;for(var r in n.propsData)t[r]=e[r];var i=n._parentListeners;for(var o in i)t[b(o)]=i[o];return t}function io(e,t){if(/\d-keep-alive$/.test(t.tag))return e("keep-alive",{props:t.componentOptions.propsData})}var oo=function(e){return e.tag||pt(e)},ao=function(e){return"show"===e.name},so={name:"transition",props:to,abstract:!0,render:function(e){var t=this,n=this.$slots.default;if(n&&(n=n.filter(oo)).length){var r=this.mode,o=n[0];if(function(e){for(;e=e.parent;)if(e.data.transition)return!0}(this.$vnode))return o;var a=no(o);if(!a)return o;if(this._leaving)return io(e,o);var s="__transition-"+this._uid+"-";a.key=null==a.key?a.isComment?s+"comment":s+a.tag:i(a.key)?0===String(a.key).indexOf(s)?a.key:s+a.key:a.key;var c=(a.data||(a.data={})).transition=ro(this),u=this._vnode,l=no(u);if(a.data.directives&&a.data.directives.some(ao)&&(a.data.show=!0),l&&l.data&&!function(e,t){return t.key===e.key&&t.tag===e.tag}(a,l)&&!pt(l)&&(!l.componentInstance||!l.componentInstance._vnode.isComment)){var f=l.data.transition=A({},c);if("out-in"===r)return this._leaving=!0,ot(f,"afterLeave",function(){t._leaving=!1,t.$forceUpdate()}),io(e,o);if("in-out"===r){if(pt(a))return u;var p,d=function(){p()};ot(c,"afterEnter",d),ot(c,"enterCancelled",d),ot(f,"delayLeave",function(e){p=e})}}return o}}},co=A({tag:String,moveClass:String},to);function uo(e){e.elm._moveCb&&e.elm._moveCb(),e.elm._enterCb&&e.elm._enterCb()}function lo(e){e.data.newPos=e.elm.getBoundingClientRect()}function fo(e){var t=e.data.pos,n=e.data.newPos,r=t.left-n.left,i=t.top-n.top;if(r||i){e.data.moved=!0;var o=e.elm.style;o.transform=o.WebkitTransform="translate("+r+"px,"+i+"px)",o.transitionDuration="0s"}}delete co.mode;var po={Transition:so,TransitionGroup:{props:co,beforeMount:function(){var e=this,t=this._update;this._update=function(n,r){var i=Gt(e);e.__patch__(e._vnode,e.kept,!1,!0),e._vnode=e.kept,i(),t.call(e,n,r)}},render:function(e){for(var t=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,i=this.$slots.default||[],o=this.children=[],a=ro(this),s=0;s-1?Xn[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:Xn[e]=/HTMLUnknownElement/.test(t.toString())},A(Cn.options.directives,eo),A(Cn.options.components,po),Cn.prototype.__patch__=V?zi:S,Cn.prototype.$mount=function(e,t){return function(e,t,n){var r;return e.$el=t,e.$options.render||(e.$options.render=ve),Qt(e,"beforeMount"),r=function(){e._update(e._render(),n)},new pn(e,r,S,{before:function(){e._isMounted&&!e._isDestroyed&&Qt(e,"beforeUpdate")}},!0),n=!1,null==e.$vnode&&(e._isMounted=!0,Qt(e,"mounted")),e}(this,e=e&&V?Qn(e):void 0,t)},V&&setTimeout(function(){F.devtools&&ne&&ne.emit("init",Cn)},0);var vo=/\{\{((?:.|\r?\n)+?)\}\}/g,ho=/[-.*+?^${}()|[\]\/\\]/g,mo=g(function(e){var t=e[0].replace(ho,"\\$&"),n=e[1].replace(ho,"\\$&");return new RegExp(t+"((?:.|\\n)+?)"+n,"g")});var yo={staticKeys:["staticClass"],transformNode:function(e,t){t.warn;var n=Pr(e,"class");n&&(e.staticClass=JSON.stringify(n));var r=Fr(e,"class",!1);r&&(e.classBinding=r)},genData:function(e){var t="";return e.staticClass&&(t+="staticClass:"+e.staticClass+","),e.classBinding&&(t+="class:"+e.classBinding+","),t}};var go,_o={staticKeys:["staticStyle"],transformNode:function(e,t){t.warn;var n=Pr(e,"style");n&&(e.staticStyle=JSON.stringify(si(n)));var r=Fr(e,"style",!1);r&&(e.styleBinding=r)},genData:function(e){var t="";return e.staticStyle&&(t+="staticStyle:"+e.staticStyle+","),e.styleBinding&&(t+="style:("+e.styleBinding+"),"),t}},bo=function(e){return(go=go||document.createElement("div")).innerHTML=e,go.textContent},$o=p("area,base,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr"),wo=p("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source"),Co=p("address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track"),xo=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,ko=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Ao="[a-zA-Z_][\\-\\.0-9_a-zA-Z"+P.source+"]*",Oo="((?:"+Ao+"\\:)?"+Ao+")",So=new RegExp("^<"+Oo),To=/^\s*(\/?)>/,No=new RegExp("^<\\/"+Oo+"[^>]*>"),Eo=/^]+>/i,jo=/^",""":'"',"&":"&"," ":"\n"," ":"\t","'":"'"},Fo=/&(?:lt|gt|quot|amp|#39);/g,Po=/&(?:lt|gt|quot|amp|#39|#10|#9);/g,Ro=p("pre,textarea",!0),Ho=function(e,t){return e&&Ro(e)&&"\n"===t[0]};function Bo(e,t){var n=t?Po:Fo;return e.replace(n,function(e){return Mo[e]})}var Uo,Vo,zo,Ko,Jo,qo,Wo,Zo,Go=/^@|^v-on:/,Xo=/^v-|^@|^:|^#/,Yo=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,Qo=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,ea=/^\(|\)$/g,ta=/^\[.*\]$/,na=/:(.*)$/,ra=/^:|^\.|^v-bind:/,ia=/\.[^.\]]+(?=[^\]]*$)/g,oa=/^v-slot(:|$)|^#/,aa=/[\r\n]/,sa=/[ \f\t\r\n]+/g,ca=g(bo),ua="_empty_";function la(e,t,n){return{type:1,tag:e,attrsList:t,attrsMap:ya(t),rawAttrsMap:{},parent:n,children:[]}}function fa(e,t){Uo=t.warn||Tr,qo=t.isPreTag||T,Wo=t.mustUseProp||T,Zo=t.getTagNamespace||T;t.isReservedTag;zo=Nr(t.modules,"transformNode"),Ko=Nr(t.modules,"preTransformNode"),Jo=Nr(t.modules,"postTransformNode"),Vo=t.delimiters;var n,r,i=[],o=!1!==t.preserveWhitespace,a=t.whitespace,s=!1,c=!1;function u(e){if(l(e),s||e.processed||(e=pa(e,t)),i.length||e===n||n.if&&(e.elseif||e.else)&&va(n,{exp:e.elseif,block:e}),r&&!e.forbidden)if(e.elseif||e.else)a=e,(u=function(e){var t=e.length;for(;t--;){if(1===e[t].type)return e[t];e.pop()}}(r.children))&&u.if&&va(u,{exp:a.elseif,block:a});else{if(e.slotScope){var o=e.slotTarget||'"default"';(r.scopedSlots||(r.scopedSlots={}))[o]=e}r.children.push(e),e.parent=r}var a,u;e.children=e.children.filter(function(e){return!e.slotScope}),l(e),e.pre&&(s=!1),qo(e.tag)&&(c=!1);for(var f=0;f]*>)","i")),p=e.replace(f,function(e,n,r){return u=r.length,Lo(l)||"noscript"===l||(n=n.replace(//g,"$1").replace(//g,"$1")),Ho(l,n)&&(n=n.slice(1)),t.chars&&t.chars(n),""});c+=e.length-p.length,e=p,A(l,c-u,c)}else{var d=e.indexOf("<");if(0===d){if(jo.test(e)){var v=e.indexOf("--\x3e");if(v>=0){t.shouldKeepComment&&t.comment(e.substring(4,v),c,c+v+3),C(v+3);continue}}if(Do.test(e)){var h=e.indexOf("]>");if(h>=0){C(h+2);continue}}var m=e.match(Eo);if(m){C(m[0].length);continue}var y=e.match(No);if(y){var g=c;C(y[0].length),A(y[1],g,c);continue}var _=x();if(_){k(_),Ho(_.tagName,e)&&C(1);continue}}var b=void 0,$=void 0,w=void 0;if(d>=0){for($=e.slice(d);!(No.test($)||So.test($)||jo.test($)||Do.test($)||(w=$.indexOf("<",1))<0);)d+=w,$=e.slice(d);b=e.substring(0,d)}d<0&&(b=e),b&&C(b.length),t.chars&&b&&t.chars(b,c-b.length,c)}if(e===n){t.chars&&t.chars(e);break}}function C(t){c+=t,e=e.substring(t)}function x(){var t=e.match(So);if(t){var n,r,i={tagName:t[1],attrs:[],start:c};for(C(t[0].length);!(n=e.match(To))&&(r=e.match(ko)||e.match(xo));)r.start=c,C(r[0].length),r.end=c,i.attrs.push(r);if(n)return i.unarySlash=n[1],C(n[0].length),i.end=c,i}}function k(e){var n=e.tagName,c=e.unarySlash;o&&("p"===r&&Co(n)&&A(r),s(n)&&r===n&&A(n));for(var u=a(n)||!!c,l=e.attrs.length,f=new Array(l),p=0;p=0&&i[a].lowerCasedTag!==s;a--);else a=0;if(a>=0){for(var u=i.length-1;u>=a;u--)t.end&&t.end(i[u].tag,n,o);i.length=a,r=a&&i[a-1].tag}else"br"===s?t.start&&t.start(e,[],!0,n,o):"p"===s&&(t.start&&t.start(e,[],!1,n,o),t.end&&t.end(e,n,o))}A()}(e,{warn:Uo,expectHTML:t.expectHTML,isUnaryTag:t.isUnaryTag,canBeLeftOpenTag:t.canBeLeftOpenTag,shouldDecodeNewlines:t.shouldDecodeNewlines,shouldDecodeNewlinesForHref:t.shouldDecodeNewlinesForHref,shouldKeepComment:t.comments,outputSourceRange:t.outputSourceRange,start:function(e,o,a,l,f){var p=r&&r.ns||Zo(e);q&&"svg"===p&&(o=function(e){for(var t=[],n=0;nc&&(s.push(o=e.slice(c,i)),a.push(JSON.stringify(o)));var u=Or(r[1].trim());a.push("_s("+u+")"),s.push({"@binding":u}),c=i+r[0].length}return c-1"+("true"===o?":("+t+")":":_q("+t+","+o+")")),Mr(e,"change","var $$a="+t+",$$el=$event.target,$$c=$$el.checked?("+o+"):("+a+");if(Array.isArray($$a)){var $$v="+(r?"_n("+i+")":i)+",$$i=_i($$a,$$v);if($$el.checked){$$i<0&&("+Ur(t,"$$a.concat([$$v])")+")}else{$$i>-1&&("+Ur(t,"$$a.slice(0,$$i).concat($$a.slice($$i+1))")+")}}else{"+Ur(t,"$$c")+"}",null,!0)}(e,r,i);else if("input"===o&&"radio"===a)!function(e,t,n){var r=n&&n.number,i=Fr(e,"value")||"null";Er(e,"checked","_q("+t+","+(i=r?"_n("+i+")":i)+")"),Mr(e,"change",Ur(t,i),null,!0)}(e,r,i);else if("input"===o||"textarea"===o)!function(e,t,n){var r=e.attrsMap.type,i=n||{},o=i.lazy,a=i.number,s=i.trim,c=!o&&"range"!==r,u=o?"change":"range"===r?Zr:"input",l="$event.target.value";s&&(l="$event.target.value.trim()"),a&&(l="_n("+l+")");var f=Ur(t,l);c&&(f="if($event.target.composing)return;"+f),Er(e,"value","("+t+")"),Mr(e,u,f,null,!0),(s||a)&&Mr(e,"blur","$forceUpdate()")}(e,r,i);else if(!F.isReservedTag(o))return Br(e,r,i),!1;return!0},text:function(e,t){t.value&&Er(e,"textContent","_s("+t.value+")",t)},html:function(e,t){t.value&&Er(e,"innerHTML","_s("+t.value+")",t)}},isPreTag:function(e){return"pre"===e},isUnaryTag:$o,mustUseProp:Dn,canBeLeftOpenTag:wo,isReservedTag:Zn,getTagNamespace:Gn,staticKeys:function(e){return e.reduce(function(e,t){return e.concat(t.staticKeys||[])},[]).join(",")}($a)},ka=g(function(e){return p("type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap"+(e?","+e:""))});function Aa(e,t){e&&(wa=ka(t.staticKeys||""),Ca=t.isReservedTag||T,function e(t){t.static=function(e){if(2===e.type)return!1;if(3===e.type)return!0;return!(!e.pre&&(e.hasBindings||e.if||e.for||d(e.tag)||!Ca(e.tag)||function(e){for(;e.parent;){if("template"!==(e=e.parent).tag)return!1;if(e.for)return!0}return!1}(e)||!Object.keys(e).every(wa)))}(t);if(1===t.type){if(!Ca(t.tag)&&"slot"!==t.tag&&null==t.attrsMap["inline-template"])return;for(var n=0,r=t.children.length;n|^function(?:\s+[\w$]+)?\s*\(/,Sa=/\([^)]*?\);*$/,Ta=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,Na={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},Ea={esc:["Esc","Escape"],tab:"Tab",enter:"Enter",space:[" ","Spacebar"],up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete","Del"]},ja=function(e){return"if("+e+")return null;"},Da={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:ja("$event.target !== $event.currentTarget"),ctrl:ja("!$event.ctrlKey"),shift:ja("!$event.shiftKey"),alt:ja("!$event.altKey"),meta:ja("!$event.metaKey"),left:ja("'button' in $event && $event.button !== 0"),middle:ja("'button' in $event && $event.button !== 1"),right:ja("'button' in $event && $event.button !== 2")};function La(e,t){var n=t?"nativeOn:":"on:",r="",i="";for(var o in e){var a=Ia(e[o]);e[o]&&e[o].dynamic?i+=o+","+a+",":r+='"'+o+'":'+a+","}return r="{"+r.slice(0,-1)+"}",i?n+"_d("+r+",["+i.slice(0,-1)+"])":n+r}function Ia(e){if(!e)return"function(){}";if(Array.isArray(e))return"["+e.map(function(e){return Ia(e)}).join(",")+"]";var t=Ta.test(e.value),n=Oa.test(e.value),r=Ta.test(e.value.replace(Sa,""));if(e.modifiers){var i="",o="",a=[];for(var s in e.modifiers)if(Da[s])o+=Da[s],Na[s]&&a.push(s);else if("exact"===s){var c=e.modifiers;o+=ja(["ctrl","shift","alt","meta"].filter(function(e){return!c[e]}).map(function(e){return"$event."+e+"Key"}).join("||"))}else a.push(s);return a.length&&(i+=function(e){return"if(!$event.type.indexOf('key')&&"+e.map(Ma).join("&&")+")return null;"}(a)),o&&(i+=o),"function($event){"+i+(t?"return "+e.value+".apply(null, arguments)":n?"return ("+e.value+").apply(null, arguments)":r?"return "+e.value:e.value)+"}"}return t||n?e.value:"function($event){"+(r?"return "+e.value:e.value)+"}"}function Ma(e){var t=parseInt(e,10);if(t)return"$event.keyCode!=="+t;var n=Na[e],r=Ea[e];return"_k($event.keyCode,"+JSON.stringify(e)+","+JSON.stringify(n)+",$event.key,"+JSON.stringify(r)+")"}var Fa={on:function(e,t){e.wrapListeners=function(e){return"_g("+e+","+t.value+")"}},bind:function(e,t){e.wrapData=function(n){return"_b("+n+",'"+e.tag+"',"+t.value+","+(t.modifiers&&t.modifiers.prop?"true":"false")+(t.modifiers&&t.modifiers.sync?",true":"")+")"}},cloak:S},Pa=function(e){this.options=e,this.warn=e.warn||Tr,this.transforms=Nr(e.modules,"transformCode"),this.dataGenFns=Nr(e.modules,"genData"),this.directives=A(A({},Fa),e.directives);var t=e.isReservedTag||T;this.maybeComponent=function(e){return!!e.component||!t(e.tag)},this.onceId=0,this.staticRenderFns=[],this.pre=!1};function Ra(e,t){var n=new Pa(t);return{render:"with(this){return "+(e?"script"===e.tag?"null":Ha(e,n):'_c("div")')+"}",staticRenderFns:n.staticRenderFns}}function Ha(e,t){if(e.parent&&(e.pre=e.pre||e.parent.pre),e.staticRoot&&!e.staticProcessed)return Ba(e,t);if(e.once&&!e.onceProcessed)return Ua(e,t);if(e.for&&!e.forProcessed)return za(e,t);if(e.if&&!e.ifProcessed)return Va(e,t);if("template"!==e.tag||e.slotTarget||t.pre){if("slot"===e.tag)return function(e,t){var n=e.slotName||'"default"',r=Wa(e,t),i="_t("+n+(r?",function(){return "+r+"}":""),o=e.attrs||e.dynamicAttrs?Xa((e.attrs||[]).concat(e.dynamicAttrs||[]).map(function(e){return{name:b(e.name),value:e.value,dynamic:e.dynamic}})):null,a=e.attrsMap["v-bind"];!o&&!a||r||(i+=",null");o&&(i+=","+o);a&&(i+=(o?"":",null")+","+a);return i+")"}(e,t);var n;if(e.component)n=function(e,t,n){var r=t.inlineTemplate?null:Wa(t,n,!0);return"_c("+e+","+Ka(t,n)+(r?","+r:"")+")"}(e.component,e,t);else{var r;(!e.plain||e.pre&&t.maybeComponent(e))&&(r=Ka(e,t));var i=e.inlineTemplate?null:Wa(e,t,!0);n="_c('"+e.tag+"'"+(r?","+r:"")+(i?","+i:"")+")"}for(var o=0;o>>0}(a):"")+")"}(e,e.scopedSlots,t)+","),e.model&&(n+="model:{value:"+e.model.value+",callback:"+e.model.callback+",expression:"+e.model.expression+"},"),e.inlineTemplate){var o=function(e,t){var n=e.children[0];if(n&&1===n.type){var r=Ra(n,t.options);return"inlineTemplate:{render:function(){"+r.render+"},staticRenderFns:["+r.staticRenderFns.map(function(e){return"function(){"+e+"}"}).join(",")+"]}"}}(e,t);o&&(n+=o+",")}return n=n.replace(/,$/,"")+"}",e.dynamicAttrs&&(n="_b("+n+',"'+e.tag+'",'+Xa(e.dynamicAttrs)+")"),e.wrapData&&(n=e.wrapData(n)),e.wrapListeners&&(n=e.wrapListeners(n)),n}function Ja(e){return 1===e.type&&("slot"===e.tag||e.children.some(Ja))}function qa(e,t){var n=e.attrsMap["slot-scope"];if(e.if&&!e.ifProcessed&&!n)return Va(e,t,qa,"null");if(e.for&&!e.forProcessed)return za(e,t,qa);var r=e.slotScope===ua?"":String(e.slotScope),i="function("+r+"){return "+("template"===e.tag?e.if&&n?"("+e.if+")?"+(Wa(e,t)||"undefined")+":undefined":Wa(e,t)||"undefined":Ha(e,t))+"}",o=r?"":",proxy:true";return"{key:"+(e.slotTarget||'"default"')+",fn:"+i+o+"}"}function Wa(e,t,n,r,i){var o=e.children;if(o.length){var a=o[0];if(1===o.length&&a.for&&"template"!==a.tag&&"slot"!==a.tag){var s=n?t.maybeComponent(a)?",1":",0":"";return""+(r||Ha)(a,t)+s}var c=n?function(e,t){for(var n=0,r=0;r':'
    ',ns.innerHTML.indexOf(" ")>0}var as=!!V&&os(!1),ss=!!V&&os(!0),cs=g(function(e){var t=Qn(e);return t&&t.innerHTML}),us=Cn.prototype.$mount;return Cn.prototype.$mount=function(e,t){if((e=e&&Qn(e))===document.body||e===document.documentElement)return this;var n=this.$options;if(!n.render){var r=n.template;if(r)if("string"==typeof r)"#"===r.charAt(0)&&(r=cs(r));else{if(!r.nodeType)return this;r=r.innerHTML}else e&&(r=function(e){if(e.outerHTML)return e.outerHTML;var t=document.createElement("div");return t.appendChild(e.cloneNode(!0)),t.innerHTML}(e));if(r){var i=is(r,{outputSourceRange:!1,shouldDecodeNewlines:as,shouldDecodeNewlinesForHref:ss,delimiters:n.delimiters,comments:n.comments},this),o=i.render,a=i.staticRenderFns;n.render=o,n.staticRenderFns=a}}return us.call(this,e,t)},Cn.compile=is,Cn}); \ No newline at end of file diff --git a/html/js/external/vue-chartjs-2.7.1.min.js b/html/js/external/vue-chartjs-3.5.1.min.js similarity index 56% rename from html/js/external/vue-chartjs-2.7.1.min.js rename to html/js/external/vue-chartjs-3.5.1.min.js index 68404551..3af1056a 100644 --- a/html/js/external/vue-chartjs-2.7.1.min.js +++ b/html/js/external/vue-chartjs-3.5.1.min.js @@ -1 +1,2 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("chart.js")):"function"==typeof define&&define.amd?define("VueChartJs",["chart.js"],e):"object"==typeof exports?exports.VueChartJs=e(require("chart.js")):t.VueChartJs=e(t.Chart)}("undefined"!=typeof self?self:this,function(t){return function(t){function e(a){if(r[a])return r[a].exports;var n=r[a]={i:a,l:!1,exports:{}};return t[a].call(n.exports,n,n.exports,e),n.l=!0,n.exports}var r={};return e.m=t,e.c=r,e.d=function(t,r,a){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:a})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=0)}([function(t,e,r){"use strict";function a(t,e){if(e){var r=this.$data._chart,a=t.datasets.map(function(t){return t.label}),n=e.datasets.map(function(t){return t.label}),s=JSON.stringify(n);JSON.stringify(a)===s&&e.datasets.length===t.datasets.length?(t.datasets.forEach(function(t,a){var n=Object.keys(e.datasets[a]),s=Object.keys(t);n.filter(function(t){return"_meta"!==t&&-1===s.indexOf(t)}).forEach(function(t){delete r.data.datasets[a][t]});for(var i in t)t.hasOwnProperty(i)&&(r.data.datasets[a][i]=t[i])}),t.hasOwnProperty("labels")&&(r.data.labels=t.labels,this.$emit("labels:update")),t.hasOwnProperty("xLabels")&&(r.data.xLabels=t.xLabels,this.$emit("xlabels:update")),t.hasOwnProperty("yLabels")&&(r.data.yLabels=t.yLabels,this.$emit("ylabels:update")),r.update(),this.$emit("chart:update")):(r&&(r.destroy(),this.$emit("chart:destroy")),this.renderChart(this.chartData,this.options),this.$emit("chart:render"))}else this.$data._chart&&(this.$data._chart.destroy(),this.$emit("chart:destroy")),this.renderChart(this.chartData,this.options),this.$emit("chart:render")}function n(t,e){return{render:function(t){return t("div",{style:this.styles,class:this.cssClasses},[t("canvas",{attrs:{id:this.chartId,width:this.width,height:this.height},ref:"canvas"})])},props:{chartId:{default:t,type:String},width:{default:400,type:Number},height:{default:400,type:Number},cssClasses:{type:String,default:""},styles:{type:Object},plugins:{type:Array,default:function(){return[]}}},data:function(){return{_chart:null,_plugins:this.plugins}},methods:{addPlugin:function(t){this.$data._plugins.push(t)},generateLegend:function(){if(this.$data._chart)return this.$data._chart.generateLegend()},renderChart:function(t,r){this.$data._chart&&this.$data._chart.destroy(),this.$data._chart=new c.a(this.$refs.canvas.getContext("2d"),{type:e,data:t,options:r,plugins:this.$data._plugins})}},beforeDestroy:function(){this.$data._chart&&this.$data._chart.destroy()}}}Object.defineProperty(e,"__esModule",{value:!0});var s={data:function(){return{chartData:null}},watch:{chartData:a}},i={props:{chartData:{type:Object,required:!0,default:function(){}}},watch:{chartData:a}},o={reactiveData:s,reactiveProp:i},u=r(1),c=r.n(u),h=n("bar-chart","bar"),d=n("horizontalbar-chart","horizontalBar"),l=n("doughnut-chart","doughnut"),f=n("line-chart","line"),p=n("pie-chart","pie"),b=n("polar-chart","polarArea"),y=n("radar-chart","radar"),g=n("bubble-chart","bubble"),m=n("scatter-chart","scatter");r.d(e,"VueCharts",function(){return v}),r.d(e,"Bar",function(){return h}),r.d(e,"HorizontalBar",function(){return d}),r.d(e,"Doughnut",function(){return l}),r.d(e,"Line",function(){return f}),r.d(e,"Pie",function(){return p}),r.d(e,"PolarArea",function(){return b}),r.d(e,"Radar",function(){return y}),r.d(e,"Bubble",function(){return g}),r.d(e,"Scatter",function(){return m}),r.d(e,"mixins",function(){return o}),r.d(e,"generateChart",function(){return n});var v={Bar:h,HorizontalBar:d,Doughnut:l,Line:f,Pie:p,PolarArea:b,Radar:y,Bubble:g,Scatter:m,mixins:o,generateChart:n,render:function(){return console.error("[vue-chartjs]: This is not a vue component. It is the whole object containing all vue components. Please import the named export or access the components over the dot notation. For more info visit https://vue-chartjs.org/#/home?id=quick-start")}};e.default=v},function(e,r){e.exports=t}])}); \ No newline at end of file +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("chart.js")):"function"==typeof define&&define.amd?define("VueChartJs",["chart.js"],e):"object"==typeof exports?exports.VueChartJs=e(require("chart.js")):t.VueChartJs=e(t.Chart)}("undefined"!=typeof self?self:this,function(t){return function(t){function e(a){if(r[a])return r[a].exports;var n=r[a]={i:a,l:!1,exports:{}};return t[a].call(n.exports,n,n.exports,e),n.l=!0,n.exports}var r={};return e.m=t,e.c=r,e.d=function(t,r,a){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:a})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=0)}([function(t,e,r){"use strict";function a(t,e){if(e){var r=this.$data._chart,a=t.datasets.map(function(t){return t.label}),n=e.datasets.map(function(t){return t.label}),s=JSON.stringify(n);JSON.stringify(a)===s&&e.datasets.length===t.datasets.length?(t.datasets.forEach(function(t,a){var n=Object.keys(e.datasets[a]),s=Object.keys(t);n.filter(function(t){return"_meta"!==t&&-1===s.indexOf(t)}).forEach(function(t){delete r.data.datasets[a][t]});for(var i in t)t.hasOwnProperty(i)&&(r.data.datasets[a][i]=t[i])}),t.hasOwnProperty("labels")&&(r.data.labels=t.labels,this.$emit("labels:update")),t.hasOwnProperty("xLabels")&&(r.data.xLabels=t.xLabels,this.$emit("xlabels:update")),t.hasOwnProperty("yLabels")&&(r.data.yLabels=t.yLabels,this.$emit("ylabels:update")),r.update(),this.$emit("chart:update")):(r&&(r.destroy(),this.$emit("chart:destroy")),this.renderChart(this.chartData,this.options),this.$emit("chart:render"))}else this.$data._chart&&(this.$data._chart.destroy(),this.$emit("chart:destroy")),this.renderChart(this.chartData,this.options),this.$emit("chart:render")}function n(t,e){return{render:function(t){return t("div",{style:this.styles,class:this.cssClasses},[t("canvas",{attrs:{id:this.chartId,width:this.width,height:this.height},ref:"canvas"})])},props:{chartId:{default:t,type:String},width:{default:400,type:Number},height:{default:400,type:Number},cssClasses:{type:String,default:""},styles:{type:Object},plugins:{type:Array,default:function(){return[]}}},data:function(){return{_chart:null,_plugins:this.plugins}},methods:{addPlugin:function(t){this.$data._plugins.push(t)},generateLegend:function(){if(this.$data._chart)return this.$data._chart.generateLegend()},renderChart:function(t,r){if(this.$data._chart&&this.$data._chart.destroy(),!this.$refs.canvas)throw new Error("Please remove the tags from your chart component. See https://vue-chartjs.org/guide/#vue-single-file-components");this.$data._chart=new c.a(this.$refs.canvas.getContext("2d"),{type:e,data:t,options:r,plugins:this.$data._plugins})}},beforeDestroy:function(){this.$data._chart&&this.$data._chart.destroy()}}}Object.defineProperty(e,"__esModule",{value:!0});var s={data:function(){return{chartData:null}},watch:{chartData:a}},i={props:{chartData:{type:Object,required:!0,default:function(){}}},watch:{chartData:a}},o={reactiveData:s,reactiveProp:i},u=r(1),c=r.n(u),h=n("bar-chart","bar"),d=n("horizontalbar-chart","horizontalBar"),l=n("doughnut-chart","doughnut"),f=n("line-chart","line"),p=n("pie-chart","pie"),b=n("polar-chart","polarArea"),y=n("radar-chart","radar"),g=n("bubble-chart","bubble"),m=n("scatter-chart","scatter");r.d(e,"VueCharts",function(){return v}),r.d(e,"Bar",function(){return h}),r.d(e,"HorizontalBar",function(){return d}),r.d(e,"Doughnut",function(){return l}),r.d(e,"Line",function(){return f}),r.d(e,"Pie",function(){return p}),r.d(e,"PolarArea",function(){return b}),r.d(e,"Radar",function(){return y}),r.d(e,"Bubble",function(){return g}),r.d(e,"Scatter",function(){return m}),r.d(e,"mixins",function(){return o}),r.d(e,"generateChart",function(){return n});var v={Bar:h,HorizontalBar:d,Doughnut:l,Line:f,Pie:p,PolarArea:b,Radar:y,Bubble:g,Scatter:m,mixins:o,generateChart:n,render:function(){return console.error("[vue-chartjs]: This is not a vue component. It is the whole object containing all vue components. Please import the named export or access the components over the dot notation. For more info visit https://vue-chartjs.org/#/home?id=quick-start")}};e.default=v},function(e,r){e.exports=t}])}); +//# sourceMappingURL=vue-chartjs.min.js.map \ No newline at end of file diff --git a/html/js/external/vue-router-3.5.1.min.js b/html/js/external/vue-router-3.5.1.min.js deleted file mode 100644 index 68b4df8d..00000000 --- a/html/js/external/vue-router-3.5.1.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * vue-router v3.5.1 - * (c) 2021 Evan You - * @license MIT - */ -var t,e;t=this,e=function(){"use strict";function t(t,e){for(var r in e)t[r]=e[r];return t}var e=/[!'()*]/g,r=function(t){return"%"+t.charCodeAt(0).toString(16)},n=/%2C/g,o=function(t){return encodeURIComponent(t).replace(e,r).replace(n,",")};function i(t){try{return decodeURIComponent(t)}catch(t){}return t}var a=function(t){return null==t||"object"==typeof t?t:String(t)};function s(t){var e={};return(t=t.trim().replace(/^(\?|#|&)/,""))?(t.split("&").forEach(function(t){var r=t.replace(/\+/g," ").split("="),n=i(r.shift()),o=r.length>0?i(r.join("=")):null;void 0===e[n]?e[n]=o:Array.isArray(e[n])?e[n].push(o):e[n]=[e[n],o]}),e):e}function u(t){var e=t?Object.keys(t).map(function(e){var r=t[e];if(void 0===r)return"";if(null===r)return o(e);if(Array.isArray(r)){var n=[];return r.forEach(function(t){void 0!==t&&(null===t?n.push(o(e)):n.push(o(e)+"="+o(t)))}),n.join("&")}return o(e)+"="+o(r)}).filter(function(t){return t.length>0}).join("&"):null;return e?"?"+e:""}var c=/\/?$/;function p(t,e,r,n){var o=n&&n.options.stringifyQuery,i=e.query||{};try{i=f(i)}catch(t){}var a={name:e.name||t&&t.name,meta:t&&t.meta||{},path:e.path||"/",hash:e.hash||"",query:i,params:e.params||{},fullPath:d(e,o),matched:t?l(t):[]};return r&&(a.redirectedFrom=d(r,o)),Object.freeze(a)}function f(t){if(Array.isArray(t))return t.map(f);if(t&&"object"==typeof t){var e={};for(var r in t)e[r]=f(t[r]);return e}return t}var h=p(null,{path:"/"});function l(t){for(var e=[];t;)e.unshift(t),t=t.parent;return e}function d(t,e){var r=t.path,n=t.query;void 0===n&&(n={});var o=t.hash;return void 0===o&&(o=""),(r||"/")+(e||u)(n)+o}function v(t,e,r){return e===h?t===e:!!e&&(t.path&&e.path?t.path.replace(c,"")===e.path.replace(c,"")&&(r||t.hash===e.hash&&y(t.query,e.query)):!(!t.name||!e.name)&&t.name===e.name&&(r||t.hash===e.hash&&y(t.query,e.query)&&y(t.params,e.params)))}function y(t,e){if(void 0===t&&(t={}),void 0===e&&(e={}),!t||!e)return t===e;var r=Object.keys(t).sort(),n=Object.keys(e).sort();return r.length===n.length&&r.every(function(r,o){var i=t[r];if(n[o]!==r)return!1;var a=e[r];return null==i||null==a?i===a:"object"==typeof i&&"object"==typeof a?y(i,a):String(i)===String(a)})}function m(t){for(var e=0;e=0&&(e=t.slice(n),t=t.slice(0,n));var o=t.indexOf("?");return o>=0&&(r=t.slice(o+1),t=t.slice(0,o)),{path:t,query:r,hash:e}}(i.path||""),h=r&&r.path||"/",l=f.path?b(f.path,h,n||i.append):h,d=function(t,e,r){void 0===e&&(e={});var n,o=r||s;try{n=o(t||"")}catch(t){n={}}for(var i in e){var u=e[i];n[i]=Array.isArray(u)?u.map(a):a(u)}return n}(f.query,i.query,o&&o.options.parseQuery),v=i.hash||f.hash;return v&&"#"!==v.charAt(0)&&(v="#"+v),{_normalized:!0,path:l,query:d,hash:v}}var H,N=[String,Object],F=[String,Array],z=function(){},D={name:"RouterLink",props:{to:{type:N,required:!0},tag:{type:String,default:"a"},custom:Boolean,exact:Boolean,exactPath:Boolean,append:Boolean,replace:Boolean,activeClass:String,exactActiveClass:String,ariaCurrentValue:{type:String,default:"page"},event:{type:F,default:"click"}},render:function(e){var r=this,n=this.$router,o=this.$route,i=n.resolve(this.to,o,this.append),a=i.location,s=i.route,u=i.href,f={},h=n.options.linkActiveClass,l=n.options.linkExactActiveClass,d=null==h?"router-link-active":h,y=null==l?"router-link-exact-active":l,m=null==this.activeClass?d:this.activeClass,g=null==this.exactActiveClass?y:this.exactActiveClass,w=s.redirectedFrom?p(null,V(s.redirectedFrom),null,n):s;f[g]=v(o,w,this.exactPath),f[m]=this.exact||this.exactPath?f[g]:function(t,e){return 0===t.path.replace(c,"/").indexOf(e.path.replace(c,"/"))&&(!e.hash||t.hash===e.hash)&&function(t,e){for(var r in e)if(!(r in t))return!1;return!0}(t.query,e.query)}(o,w);var b=f[g]?this.ariaCurrentValue:null,x=function(t){K(t)&&(r.replace?n.replace(a,z):n.push(a,z))},R={click:K};Array.isArray(this.event)?this.event.forEach(function(t){R[t]=x}):R[this.event]=x;var k={class:f},E=!this.$scopedSlots.$hasNormal&&this.$scopedSlots.default&&this.$scopedSlots.default({href:u,route:s,navigate:x,isActive:f[m],isExactActive:f[g]});if(E){if(1===E.length)return E[0];if(E.length>1||!E.length)return 0===E.length?e():e("span",{},E)}if("a"===this.tag)k.on=R,k.attrs={href:u,"aria-current":b};else{var C=function t(e){if(e)for(var r,n=0;n-1&&(s.params[h]=r.params[h]);return s.path=M(p.path,s.params),u(p,s,a)}if(s.path){s.params={};for(var l=0;l=t.length?r():t[o]?e(t[o],function(){n(o+1)}):n(o+1)};n(0)}var gt={redirected:2,aborted:4,cancelled:8,duplicated:16};function wt(t,e){return xt(t,e,gt.redirected,'Redirected when going from "'+t.fullPath+'" to "'+function(t){if("string"==typeof t)return t;if("path"in t)return t.path;var e={};return Rt.forEach(function(r){r in t&&(e[r]=t[r])}),JSON.stringify(e,null,2)}(e)+'" via a navigation guard.')}function bt(t,e){return xt(t,e,gt.cancelled,'Navigation cancelled from "'+t.fullPath+'" to "'+e.fullPath+'" with a new navigation.')}function xt(t,e,r,n){var o=new Error(n);return o._isRouter=!0,o.from=t,o.to=e,o.type=r,o}var Rt=["params","query","hash"];function kt(t){return Object.prototype.toString.call(t).indexOf("Error")>-1}function Et(t,e){return kt(t)&&t._isRouter&&(null==e||t.type===e)}function Ct(t){return function(e,r,n){var o=!1,i=0,a=null;At(t,function(t,e,r,s){if("function"==typeof t&&void 0===t.cid){o=!0,i++;var u,c=jt(function(e){var o;((o=e).__esModule||_t&&"Module"===o[Symbol.toStringTag])&&(e=e.default),t.resolved="function"==typeof e?e:H.extend(e),r.components[s]=e,--i<=0&&n()}),p=jt(function(t){var e="Failed to resolve async component "+s+": "+t;a||(a=kt(t)?t:new Error(e),n(a))});try{u=t(c,p)}catch(t){p(t)}if(u)if("function"==typeof u.then)u.then(c,p);else{var f=u.component;f&&"function"==typeof f.then&&f.then(c,p)}}}),o||n()}}function At(t,e){return Ot(t.map(function(t){return Object.keys(t.components).map(function(r){return e(t.components[r],t.instances[r],t,r)})}))}function Ot(t){return Array.prototype.concat.apply([],t)}var _t="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag;function jt(t){var e=!1;return function(){for(var r=[],n=arguments.length;n--;)r[n]=arguments[n];if(!e)return e=!0,t.apply(this,r)}}var Tt=function(t,e){this.router=t,this.base=function(t){if(!t)if(J){var e=document.querySelector("base");t=(t=e&&e.getAttribute("href")||"/").replace(/^https?:\/\/[^\/]+/,"")}else t="/";return"/"!==t.charAt(0)&&(t="/"+t),t.replace(/\/$/,"")}(e),this.current=h,this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[],this.listeners=[]};function St(t,e,r,n){var o=At(t,function(t,n,o,i){var a=function(t,e){return"function"!=typeof t&&(t=H.extend(t)),t.options[e]}(t,e);if(a)return Array.isArray(a)?a.map(function(t){return r(t,n,o,i)}):r(a,n,o,i)});return Ot(n?o.reverse():o)}function Pt(t,e){if(e)return function(){return t.apply(e,arguments)}}Tt.prototype.listen=function(t){this.cb=t},Tt.prototype.onReady=function(t,e){this.ready?t():(this.readyCbs.push(t),e&&this.readyErrorCbs.push(e))},Tt.prototype.onError=function(t){this.errorCbs.push(t)},Tt.prototype.transitionTo=function(t,e,r){var n,o=this;try{n=this.router.match(t,this.current)}catch(t){throw this.errorCbs.forEach(function(e){e(t)}),t}var i=this.current;this.confirmTransition(n,function(){o.updateRoute(n),e&&e(n),o.ensureURL(),o.router.afterHooks.forEach(function(t){t&&t(n,i)}),o.ready||(o.ready=!0,o.readyCbs.forEach(function(t){t(n)}))},function(t){r&&r(t),t&&!o.ready&&(Et(t,gt.redirected)&&i===h||(o.ready=!0,o.readyErrorCbs.forEach(function(e){e(t)})))})},Tt.prototype.confirmTransition=function(t,e,r){var n=this,o=this.current;this.pending=t;var i,a,s=function(t){!Et(t)&&kt(t)&&(n.errorCbs.length?n.errorCbs.forEach(function(e){e(t)}):console.error(t)),r&&r(t)},u=t.matched.length-1,c=o.matched.length-1;if(v(t,o)&&u===c&&t.matched[u]===o.matched[c])return this.ensureURL(),s(((a=xt(i=o,t,gt.duplicated,'Avoided redundant navigation to current location: "'+i.fullPath+'".')).name="NavigationDuplicated",a));var p=function(t,e){var r,n=Math.max(t.length,e.length);for(r=0;r0)){var e=this.router,r=e.options.scrollBehavior,n=dt&&r;n&&this.listeners.push(ot());var o=function(){var r=t.current,o=$t(t.base);t.current===h&&o===t._startLocation||t.transitionTo(o,function(t){n&&it(e,t,r,!0)})};window.addEventListener("popstate",o),this.listeners.push(function(){window.removeEventListener("popstate",o)})}},e.prototype.go=function(t){window.history.go(t)},e.prototype.push=function(t,e,r){var n=this,o=this.current;this.transitionTo(t,function(t){vt(x(n.base+t.fullPath)),it(n.router,t,o,!1),e&&e(t)},r)},e.prototype.replace=function(t,e,r){var n=this,o=this.current;this.transitionTo(t,function(t){yt(x(n.base+t.fullPath)),it(n.router,t,o,!1),e&&e(t)},r)},e.prototype.ensureURL=function(t){if($t(this.base)!==this.current.fullPath){var e=x(this.base+this.current.fullPath);t?vt(e):yt(e)}},e.prototype.getCurrentLocation=function(){return $t(this.base)},e}(Tt);function $t(t){var e=window.location.pathname;return t&&0===e.toLowerCase().indexOf(t.toLowerCase())&&(e=e.slice(t.length)),(e||"/")+window.location.search+window.location.hash}var qt=function(t){function e(e,r,n){t.call(this,e,r),n&&function(t){var e=$t(t);if(!/^\/#/.test(e))return window.location.replace(x(t+"/#"+e)),!0}(this.base)||Ut()}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.setupListeners=function(){var t=this;if(!(this.listeners.length>0)){var e=this.router.options.scrollBehavior,r=dt&&e;r&&this.listeners.push(ot());var n=function(){var e=t.current;Ut()&&t.transitionTo(Bt(),function(n){r&&it(t.router,n,e,!0),dt||Vt(n.fullPath)})},o=dt?"popstate":"hashchange";window.addEventListener(o,n),this.listeners.push(function(){window.removeEventListener(o,n)})}},e.prototype.push=function(t,e,r){var n=this,o=this.current;this.transitionTo(t,function(t){Mt(t.fullPath),it(n.router,t,o,!1),e&&e(t)},r)},e.prototype.replace=function(t,e,r){var n=this,o=this.current;this.transitionTo(t,function(t){Vt(t.fullPath),it(n.router,t,o,!1),e&&e(t)},r)},e.prototype.go=function(t){window.history.go(t)},e.prototype.ensureURL=function(t){var e=this.current.fullPath;Bt()!==e&&(t?Mt(e):Vt(e))},e.prototype.getCurrentLocation=function(){return Bt()},e}(Tt);function Ut(){var t=Bt();return"/"===t.charAt(0)||(Vt("/"+t),!1)}function Bt(){var t=window.location.href,e=t.indexOf("#");return e<0?"":t=t.slice(e+1)}function It(t){var e=window.location.href,r=e.indexOf("#");return(r>=0?e.slice(0,r):e)+"#"+t}function Mt(t){dt?vt(It(t)):window.location.hash=t}function Vt(t){dt?yt(It(t)):window.location.replace(It(t))}var Ht=function(t){function e(e,r){t.call(this,e,r),this.stack=[],this.index=-1}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.push=function(t,e,r){var n=this;this.transitionTo(t,function(t){n.stack=n.stack.slice(0,n.index+1).concat(t),n.index++,e&&e(t)},r)},e.prototype.replace=function(t,e,r){var n=this;this.transitionTo(t,function(t){n.stack=n.stack.slice(0,n.index).concat(t),e&&e(t)},r)},e.prototype.go=function(t){var e=this,r=this.index+t;if(!(r<0||r>=this.stack.length)){var n=this.stack[r];this.confirmTransition(n,function(){var t=e.current;e.index=r,e.updateRoute(n),e.router.afterHooks.forEach(function(e){e&&e(n,t)})},function(t){Et(t,gt.duplicated)&&(e.index=r)})}},e.prototype.getCurrentLocation=function(){var t=this.stack[this.stack.length-1];return t?t.fullPath:"/"},e.prototype.ensureURL=function(){},e}(Tt),Nt=function(t){void 0===t&&(t={}),this.app=null,this.apps=[],this.options=t,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=Y(t.routes||[],this);var e=t.mode||"hash";switch(this.fallback="history"===e&&!dt&&!1!==t.fallback,this.fallback&&(e="hash"),J||(e="abstract"),this.mode=e,e){case"history":this.history=new Lt(this,t.base);break;case"hash":this.history=new qt(this,t.base,this.fallback);break;case"abstract":this.history=new Ht(this,t.base)}},Ft={currentRoute:{configurable:!0}};function zt(t,e){return t.push(e),function(){var r=t.indexOf(e);r>-1&&t.splice(r,1)}}return Nt.prototype.match=function(t,e,r){return this.matcher.match(t,e,r)},Ft.currentRoute.get=function(){return this.history&&this.history.current},Nt.prototype.init=function(t){var e=this;if(this.apps.push(t),t.$once("hook:destroyed",function(){var r=e.apps.indexOf(t);r>-1&&e.apps.splice(r,1),e.app===t&&(e.app=e.apps[0]||null),e.app||e.history.teardown()}),!this.app){this.app=t;var r=this.history;if(r instanceof Lt||r instanceof qt){var n=function(t){r.setupListeners(),function(t){var n=r.current,o=e.options.scrollBehavior;dt&&o&&"fullPath"in t&&it(e,t,n,!1)}(t)};r.transitionTo(r.getCurrentLocation(),n,n)}r.listen(function(t){e.apps.forEach(function(e){e._route=t})})}},Nt.prototype.beforeEach=function(t){return zt(this.beforeHooks,t)},Nt.prototype.beforeResolve=function(t){return zt(this.resolveHooks,t)},Nt.prototype.afterEach=function(t){return zt(this.afterHooks,t)},Nt.prototype.onReady=function(t,e){this.history.onReady(t,e)},Nt.prototype.onError=function(t){this.history.onError(t)},Nt.prototype.push=function(t,e,r){var n=this;if(!e&&!r&&"undefined"!=typeof Promise)return new Promise(function(e,r){n.history.push(t,e,r)});this.history.push(t,e,r)},Nt.prototype.replace=function(t,e,r){var n=this;if(!e&&!r&&"undefined"!=typeof Promise)return new Promise(function(e,r){n.history.replace(t,e,r)});this.history.replace(t,e,r)},Nt.prototype.go=function(t){this.history.go(t)},Nt.prototype.back=function(){this.go(-1)},Nt.prototype.forward=function(){this.go(1)},Nt.prototype.getMatchedComponents=function(t){var e=t?t.matched?t:this.resolve(t).route:this.currentRoute;return e?[].concat.apply([],e.matched.map(function(t){return Object.keys(t.components).map(function(e){return t.components[e]})})):[]},Nt.prototype.resolve=function(t,e,r){var n=V(t,e=e||this.history.current,r,this),o=this.match(n,e),i=o.redirectedFrom||o.fullPath;return{location:n,route:o,href:function(t,e,r){var n="hash"===r?"#"+e:e;return t?x(t+"/"+n):n}(this.history.base,i,this.mode),normalizedTo:n,resolved:o}},Nt.prototype.getRoutes=function(){return this.matcher.getRoutes()},Nt.prototype.addRoute=function(t,e){this.matcher.addRoute(t,e),this.history.current!==h&&this.history.transitionTo(this.history.getCurrentLocation())},Nt.prototype.addRoutes=function(t){this.matcher.addRoutes(t),this.history.current!==h&&this.history.transitionTo(this.history.getCurrentLocation())},Object.defineProperties(Nt.prototype,Ft),Nt.install=function t(e){if(!t.installed||H!==e){t.installed=!0,H=e;var r=function(t){return void 0!==t},n=function(t,e){var n=t.$options._parentVnode;r(n)&&r(n=n.data)&&r(n=n.registerRouteInstance)&&n(t,e)};e.mixin({beforeCreate:function(){r(this.$options.router)?(this._routerRoot=this,this._router=this.$options.router,this._router.init(this),e.util.defineReactive(this,"_route",this._router.history.current)):this._routerRoot=this.$parent&&this.$parent._routerRoot||this,n(this,this)},destroyed:function(){n(this)}}),Object.defineProperty(e.prototype,"$router",{get:function(){return this._routerRoot._router}}),Object.defineProperty(e.prototype,"$route",{get:function(){return this._routerRoot._route}}),e.component("RouterView",g),e.component("RouterLink",D);var o=e.config.optionMergeStrategies;o.beforeRouteEnter=o.beforeRouteLeave=o.beforeRouteUpdate=o.created}},Nt.version="3.5.1",Nt.isNavigationFailure=Et,Nt.NavigationFailureType=gt,Nt.START_LOCATION=h,J&&window.Vue&&window.Vue.use(Nt),Nt},"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).VueRouter=e(); \ No newline at end of file diff --git a/html/js/external/vue-router-3.5.2.min.js b/html/js/external/vue-router-3.5.2.min.js new file mode 100644 index 00000000..d5d422aa --- /dev/null +++ b/html/js/external/vue-router-3.5.2.min.js @@ -0,0 +1,11 @@ +/*! + * vue-router v3.5.2 + * (c) 2021 Evan You + * @license MIT + */ +/*! + * vue-router v3.5.2 + * (c) 2021 Evan You + * @license MIT + */ +var t,e;t=this,e=function(){"use strict";function t(t,e){for(var r in e)t[r]=e[r];return t}var e=/[!'()*]/g,r=function(t){return"%"+t.charCodeAt(0).toString(16)},n=/%2C/g,o=function(t){return encodeURIComponent(t).replace(e,r).replace(n,",")};function i(t){try{return decodeURIComponent(t)}catch(t){}return t}var a=function(t){return null==t||"object"==typeof t?t:String(t)};function s(t){var e={};return(t=t.trim().replace(/^(\?|#|&)/,""))?(t.split("&").forEach((function(t){var r=t.replace(/\+/g," ").split("="),n=i(r.shift()),o=r.length>0?i(r.join("=")):null;void 0===e[n]?e[n]=o:Array.isArray(e[n])?e[n].push(o):e[n]=[e[n],o]})),e):e}function u(t){var e=t?Object.keys(t).map((function(e){var r=t[e];if(void 0===r)return"";if(null===r)return o(e);if(Array.isArray(r)){var n=[];return r.forEach((function(t){void 0!==t&&(null===t?n.push(o(e)):n.push(o(e)+"="+o(t)))})),n.join("&")}return o(e)+"="+o(r)})).filter((function(t){return t.length>0})).join("&"):null;return e?"?"+e:""}var c=/\/?$/;function p(t,e,r,n){var o=n&&n.options.stringifyQuery,i=e.query||{};try{i=f(i)}catch(t){}var a={name:e.name||t&&t.name,meta:t&&t.meta||{},path:e.path||"/",hash:e.hash||"",query:i,params:e.params||{},fullPath:d(e,o),matched:t?l(t):[]};return r&&(a.redirectedFrom=d(r,o)),Object.freeze(a)}function f(t){if(Array.isArray(t))return t.map(f);if(t&&"object"==typeof t){var e={};for(var r in t)e[r]=f(t[r]);return e}return t}var h=p(null,{path:"/"});function l(t){for(var e=[];t;)e.unshift(t),t=t.parent;return e}function d(t,e){var r=t.path,n=t.query;void 0===n&&(n={});var o=t.hash;return void 0===o&&(o=""),(r||"/")+(e||u)(n)+o}function v(t,e,r){return e===h?t===e:!!e&&(t.path&&e.path?t.path.replace(c,"")===e.path.replace(c,"")&&(r||t.hash===e.hash&&y(t.query,e.query)):!(!t.name||!e.name)&&t.name===e.name&&(r||t.hash===e.hash&&y(t.query,e.query)&&y(t.params,e.params)))}function y(t,e){if(void 0===t&&(t={}),void 0===e&&(e={}),!t||!e)return t===e;var r=Object.keys(t).sort(),n=Object.keys(e).sort();return r.length===n.length&&r.every((function(r,o){var i=t[r];if(n[o]!==r)return!1;var a=e[r];return null==i||null==a?i===a:"object"==typeof i&&"object"==typeof a?y(i,a):String(i)===String(a)}))}function m(t){for(var e=0;e=0&&(e=t.slice(n),t=t.slice(0,n));var o=t.indexOf("?");return o>=0&&(r=t.slice(o+1),t=t.slice(0,o)),{path:t,query:r,hash:e}}(i.path||""),h=r&&r.path||"/",l=f.path?b(f.path,h,n||i.append):h,d=function(t,e,r){void 0===e&&(e={});var n,o=r||s;try{n=o(t||"")}catch(t){n={}}for(var i in e){var u=e[i];n[i]=Array.isArray(u)?u.map(a):a(u)}return n}(f.query,i.query,o&&o.options.parseQuery),v=i.hash||f.hash;return v&&"#"!==v.charAt(0)&&(v="#"+v),{_normalized:!0,path:l,query:d,hash:v}}var H,N=function(){},F={name:"RouterLink",props:{to:{type:[String,Object],required:!0},tag:{type:String,default:"a"},custom:Boolean,exact:Boolean,exactPath:Boolean,append:Boolean,replace:Boolean,activeClass:String,exactActiveClass:String,ariaCurrentValue:{type:String,default:"page"},event:{type:[String,Array],default:"click"}},render:function(e){var r=this,n=this.$router,o=this.$route,i=n.resolve(this.to,o,this.append),a=i.location,s=i.route,u=i.href,f={},h=n.options.linkActiveClass,l=n.options.linkExactActiveClass,d=null==h?"router-link-active":h,y=null==l?"router-link-exact-active":l,m=null==this.activeClass?d:this.activeClass,g=null==this.exactActiveClass?y:this.exactActiveClass,w=s.redirectedFrom?p(null,V(s.redirectedFrom),null,n):s;f[g]=v(o,w,this.exactPath),f[m]=this.exact||this.exactPath?f[g]:function(t,e){return 0===t.path.replace(c,"/").indexOf(e.path.replace(c,"/"))&&(!e.hash||t.hash===e.hash)&&function(t,e){for(var r in e)if(!(r in t))return!1;return!0}(t.query,e.query)}(o,w);var b=f[g]?this.ariaCurrentValue:null,x=function(t){z(t)&&(r.replace?n.replace(a,N):n.push(a,N))},R={click:z};Array.isArray(this.event)?this.event.forEach((function(t){R[t]=x})):R[this.event]=x;var k={class:f},E=!this.$scopedSlots.$hasNormal&&this.$scopedSlots.default&&this.$scopedSlots.default({href:u,route:s,navigate:x,isActive:f[m],isExactActive:f[g]});if(E){if(1===E.length)return E[0];if(E.length>1||!E.length)return 0===E.length?e():e("span",{},E)}if("a"===this.tag)k.on=R,k.attrs={href:u,"aria-current":b};else{var C=function t(e){var r;if(e)for(var n=0;n-1&&(s.params[h]=r.params[h]);return s.path=M(p.path,s.params),u(p,s,a)}if(s.path){s.params={};for(var l=0;l=t.length?r():t[o]?e(t[o],(function(){n(o+1)})):n(o+1)};n(0)}var yt={redirected:2,aborted:4,cancelled:8,duplicated:16};function mt(t,e){return wt(t,e,yt.redirected,'Redirected when going from "'+t.fullPath+'" to "'+function(t){if("string"==typeof t)return t;if("path"in t)return t.path;var e={};return bt.forEach((function(r){r in t&&(e[r]=t[r])})),JSON.stringify(e,null,2)}(e)+'" via a navigation guard.')}function gt(t,e){return wt(t,e,yt.cancelled,'Navigation cancelled from "'+t.fullPath+'" to "'+e.fullPath+'" with a new navigation.')}function wt(t,e,r,n){var o=new Error(n);return o._isRouter=!0,o.from=t,o.to=e,o.type=r,o}var bt=["params","query","hash"];function xt(t){return Object.prototype.toString.call(t).indexOf("Error")>-1}function Rt(t,e){return xt(t)&&t._isRouter&&(null==e||t.type===e)}function kt(t){return function(e,r,n){var o=!1,i=0,a=null;Et(t,(function(t,e,r,s){if("function"==typeof t&&void 0===t.cid){o=!0,i++;var u,c=Ot((function(e){var o;((o=e).__esModule||At&&"Module"===o[Symbol.toStringTag])&&(e=e.default),t.resolved="function"==typeof e?e:H.extend(e),r.components[s]=e,--i<=0&&n()})),p=Ot((function(t){var e="Failed to resolve async component "+s+": "+t;a||(a=xt(t)?t:new Error(e),n(a))}));try{u=t(c,p)}catch(t){p(t)}if(u)if("function"==typeof u.then)u.then(c,p);else{var f=u.component;f&&"function"==typeof f.then&&f.then(c,p)}}})),o||n()}}function Et(t,e){return Ct(t.map((function(t){return Object.keys(t.components).map((function(r){return e(t.components[r],t.instances[r],t,r)}))})))}function Ct(t){return Array.prototype.concat.apply([],t)}var At="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag;function Ot(t){var e=!1;return function(){for(var r=[],n=arguments.length;n--;)r[n]=arguments[n];if(!e)return e=!0,t.apply(this,r)}}var _t=function(t,e){this.router=t,this.base=function(t){if(!t)if(D){var e=document.querySelector("base");t=(t=e&&e.getAttribute("href")||"/").replace(/^https?:\/\/[^\/]+/,"")}else t="/";return"/"!==t.charAt(0)&&(t="/"+t),t.replace(/\/$/,"")}(e),this.current=h,this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[],this.listeners=[]};function jt(t,e,r,n){var o=Et(t,(function(t,n,o,i){var a=function(t,e){return"function"!=typeof t&&(t=H.extend(t)),t.options[e]}(t,e);if(a)return Array.isArray(a)?a.map((function(t){return r(t,n,o,i)})):r(a,n,o,i)}));return Ct(n?o.reverse():o)}function Tt(t,e){if(e)return function(){return t.apply(e,arguments)}}_t.prototype.listen=function(t){this.cb=t},_t.prototype.onReady=function(t,e){this.ready?t():(this.readyCbs.push(t),e&&this.readyErrorCbs.push(e))},_t.prototype.onError=function(t){this.errorCbs.push(t)},_t.prototype.transitionTo=function(t,e,r){var n,o=this;try{n=this.router.match(t,this.current)}catch(t){throw this.errorCbs.forEach((function(e){e(t)})),t}var i=this.current;this.confirmTransition(n,(function(){o.updateRoute(n),e&&e(n),o.ensureURL(),o.router.afterHooks.forEach((function(t){t&&t(n,i)})),o.ready||(o.ready=!0,o.readyCbs.forEach((function(t){t(n)})))}),(function(t){r&&r(t),t&&!o.ready&&(Rt(t,yt.redirected)&&i===h||(o.ready=!0,o.readyErrorCbs.forEach((function(e){e(t)}))))}))},_t.prototype.confirmTransition=function(t,e,r){var n=this,o=this.current;this.pending=t;var i,a,s=function(t){!Rt(t)&&xt(t)&&(n.errorCbs.length?n.errorCbs.forEach((function(e){e(t)})):console.error(t)),r&&r(t)},u=t.matched.length-1,c=o.matched.length-1;if(v(t,o)&&u===c&&t.matched[u]===o.matched[c])return this.ensureURL(),s(((a=wt(i=o,t,yt.duplicated,'Avoided redundant navigation to current location: "'+i.fullPath+'".')).name="NavigationDuplicated",a));var p=function(t,e){var r,n=Math.max(t.length,e.length);for(r=0;r0)){var e=this.router,r=e.options.scrollBehavior,n=ht&&r;n&&this.listeners.push(rt());var o=function(){var r=t.current,o=Pt(t.base);t.current===h&&o===t._startLocation||t.transitionTo(o,(function(t){n&&nt(e,t,r,!0)}))};window.addEventListener("popstate",o),this.listeners.push((function(){window.removeEventListener("popstate",o)}))}},e.prototype.go=function(t){window.history.go(t)},e.prototype.push=function(t,e,r){var n=this,o=this.current;this.transitionTo(t,(function(t){lt(x(n.base+t.fullPath)),nt(n.router,t,o,!1),e&&e(t)}),r)},e.prototype.replace=function(t,e,r){var n=this,o=this.current;this.transitionTo(t,(function(t){dt(x(n.base+t.fullPath)),nt(n.router,t,o,!1),e&&e(t)}),r)},e.prototype.ensureURL=function(t){if(Pt(this.base)!==this.current.fullPath){var e=x(this.base+this.current.fullPath);t?lt(e):dt(e)}},e.prototype.getCurrentLocation=function(){return Pt(this.base)},e}(_t);function Pt(t){var e=window.location.pathname,r=e.toLowerCase(),n=t.toLowerCase();return!t||r!==n&&0!==r.indexOf(x(n+"/"))||(e=e.slice(t.length)),(e||"/")+window.location.search+window.location.hash}var Lt=function(t){function e(e,r,n){t.call(this,e,r),n&&function(t){var e=Pt(t);if(!/^\/#/.test(e))return window.location.replace(x(t+"/#"+e)),!0}(this.base)||$t()}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.setupListeners=function(){var t=this;if(!(this.listeners.length>0)){var e=this.router.options.scrollBehavior,r=ht&&e;r&&this.listeners.push(rt());var n=function(){var e=t.current;$t()&&t.transitionTo(qt(),(function(n){r&&nt(t.router,n,e,!0),ht||It(n.fullPath)}))},o=ht?"popstate":"hashchange";window.addEventListener(o,n),this.listeners.push((function(){window.removeEventListener(o,n)}))}},e.prototype.push=function(t,e,r){var n=this,o=this.current;this.transitionTo(t,(function(t){Bt(t.fullPath),nt(n.router,t,o,!1),e&&e(t)}),r)},e.prototype.replace=function(t,e,r){var n=this,o=this.current;this.transitionTo(t,(function(t){It(t.fullPath),nt(n.router,t,o,!1),e&&e(t)}),r)},e.prototype.go=function(t){window.history.go(t)},e.prototype.ensureURL=function(t){var e=this.current.fullPath;qt()!==e&&(t?Bt(e):It(e))},e.prototype.getCurrentLocation=function(){return qt()},e}(_t);function $t(){var t=qt();return"/"===t.charAt(0)||(It("/"+t),!1)}function qt(){var t=window.location.href,e=t.indexOf("#");return e<0?"":t=t.slice(e+1)}function Ut(t){var e=window.location.href,r=e.indexOf("#");return(r>=0?e.slice(0,r):e)+"#"+t}function Bt(t){ht?lt(Ut(t)):window.location.hash=t}function It(t){ht?dt(Ut(t)):window.location.replace(Ut(t))}var Mt=function(t){function e(e,r){t.call(this,e,r),this.stack=[],this.index=-1}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.push=function(t,e,r){var n=this;this.transitionTo(t,(function(t){n.stack=n.stack.slice(0,n.index+1).concat(t),n.index++,e&&e(t)}),r)},e.prototype.replace=function(t,e,r){var n=this;this.transitionTo(t,(function(t){n.stack=n.stack.slice(0,n.index).concat(t),e&&e(t)}),r)},e.prototype.go=function(t){var e=this,r=this.index+t;if(!(r<0||r>=this.stack.length)){var n=this.stack[r];this.confirmTransition(n,(function(){var t=e.current;e.index=r,e.updateRoute(n),e.router.afterHooks.forEach((function(e){e&&e(n,t)}))}),(function(t){Rt(t,yt.duplicated)&&(e.index=r)}))}},e.prototype.getCurrentLocation=function(){var t=this.stack[this.stack.length-1];return t?t.fullPath:"/"},e.prototype.ensureURL=function(){},e}(_t),Vt=function(t){void 0===t&&(t={}),this.app=null,this.apps=[],this.options=t,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=Q(t.routes||[],this);var e=t.mode||"hash";switch(this.fallback="history"===e&&!ht&&!1!==t.fallback,this.fallback&&(e="hash"),D||(e="abstract"),this.mode=e,e){case"history":this.history=new St(this,t.base);break;case"hash":this.history=new Lt(this,t.base,this.fallback);break;case"abstract":this.history=new Mt(this,t.base)}},Ht={currentRoute:{configurable:!0}};function Nt(t,e){return t.push(e),function(){var r=t.indexOf(e);r>-1&&t.splice(r,1)}}return Vt.prototype.match=function(t,e,r){return this.matcher.match(t,e,r)},Ht.currentRoute.get=function(){return this.history&&this.history.current},Vt.prototype.init=function(t){var e=this;if(this.apps.push(t),t.$once("hook:destroyed",(function(){var r=e.apps.indexOf(t);r>-1&&e.apps.splice(r,1),e.app===t&&(e.app=e.apps[0]||null),e.app||e.history.teardown()})),!this.app){this.app=t;var r=this.history;if(r instanceof St||r instanceof Lt){var n=function(t){r.setupListeners(),function(t){var n=r.current,o=e.options.scrollBehavior;ht&&o&&"fullPath"in t&&nt(e,t,n,!1)}(t)};r.transitionTo(r.getCurrentLocation(),n,n)}r.listen((function(t){e.apps.forEach((function(e){e._route=t}))}))}},Vt.prototype.beforeEach=function(t){return Nt(this.beforeHooks,t)},Vt.prototype.beforeResolve=function(t){return Nt(this.resolveHooks,t)},Vt.prototype.afterEach=function(t){return Nt(this.afterHooks,t)},Vt.prototype.onReady=function(t,e){this.history.onReady(t,e)},Vt.prototype.onError=function(t){this.history.onError(t)},Vt.prototype.push=function(t,e,r){var n=this;if(!e&&!r&&"undefined"!=typeof Promise)return new Promise((function(e,r){n.history.push(t,e,r)}));this.history.push(t,e,r)},Vt.prototype.replace=function(t,e,r){var n=this;if(!e&&!r&&"undefined"!=typeof Promise)return new Promise((function(e,r){n.history.replace(t,e,r)}));this.history.replace(t,e,r)},Vt.prototype.go=function(t){this.history.go(t)},Vt.prototype.back=function(){this.go(-1)},Vt.prototype.forward=function(){this.go(1)},Vt.prototype.getMatchedComponents=function(t){var e=t?t.matched?t:this.resolve(t).route:this.currentRoute;return e?[].concat.apply([],e.matched.map((function(t){return Object.keys(t.components).map((function(e){return t.components[e]}))}))):[]},Vt.prototype.resolve=function(t,e,r){var n=V(t,e=e||this.history.current,r,this),o=this.match(n,e),i=o.redirectedFrom||o.fullPath;return{location:n,route:o,href:function(t,e,r){var n="hash"===r?"#"+e:e;return t?x(t+"/"+n):n}(this.history.base,i,this.mode),normalizedTo:n,resolved:o}},Vt.prototype.getRoutes=function(){return this.matcher.getRoutes()},Vt.prototype.addRoute=function(t,e){this.matcher.addRoute(t,e),this.history.current!==h&&this.history.transitionTo(this.history.getCurrentLocation())},Vt.prototype.addRoutes=function(t){this.matcher.addRoutes(t),this.history.current!==h&&this.history.transitionTo(this.history.getCurrentLocation())},Object.defineProperties(Vt.prototype,Ht),Vt.install=function t(e){if(!t.installed||H!==e){t.installed=!0,H=e;var r=function(t){return void 0!==t},n=function(t,e){var n=t.$options._parentVnode;r(n)&&r(n=n.data)&&r(n=n.registerRouteInstance)&&n(t,e)};e.mixin({beforeCreate:function(){r(this.$options.router)?(this._routerRoot=this,this._router=this.$options.router,this._router.init(this),e.util.defineReactive(this,"_route",this._router.history.current)):this._routerRoot=this.$parent&&this.$parent._routerRoot||this,n(this,this)},destroyed:function(){n(this)}}),Object.defineProperty(e.prototype,"$router",{get:function(){return this._routerRoot._router}}),Object.defineProperty(e.prototype,"$route",{get:function(){return this._routerRoot._route}}),e.component("RouterView",g),e.component("RouterLink",F);var o=e.config.optionMergeStrategies;o.beforeRouteEnter=o.beforeRouteLeave=o.beforeRouteUpdate=o.created}},Vt.version="3.5.2",Vt.isNavigationFailure=Rt,Vt.NavigationFailureType=yt,Vt.START_LOCATION=h,D&&window.Vue&&window.Vue.use(Vt),Vt},"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).VueRouter=e(); \ No newline at end of file diff --git a/html/js/external/vuetify-v2.4.7.min.js b/html/js/external/vuetify-v2.4.7.min.js deleted file mode 100644 index 3dc62074..00000000 --- a/html/js/external/vuetify-v2.4.7.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! -* Vuetify v2.4.7 -* Forged by John Leider -* Released under the MIT License. -*/ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("vue")):"function"==typeof define&&define.amd?define(["vue"],e):"object"==typeof exports?exports.Vuetify=e(require("vue")):t.Vuetify=e(t.Vue)}("undefined"!=typeof self?self:this,function(t){return function(t){var e={};function i(n){if(e[n])return e[n].exports;var r=e[n]={i:n,l:!1,exports:{}};return t[n].call(r.exports,r,r.exports,i),r.l=!0,r.exports}return i.m=t,i.c=e,i.d=function(t,e,n){i.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},i.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},i.t=function(t,e){if(1&e&&(t=i(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)i.d(n,r,function(e){return t[e]}.bind(null,r));return n},i.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return i.d(e,"a",e),e},i.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},i.p="/dist/",i(i.s=97)}([function(e,i){e.exports=t},,function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){},function(t,e,i){"use strict";i.r(e);var n={};i.r(n),i.d(n,"linear",function(){return ut}),i.d(n,"easeInQuad",function(){return ct}),i.d(n,"easeOutQuad",function(){return ht}),i.d(n,"easeInOutQuad",function(){return dt}),i.d(n,"easeInCubic",function(){return pt}),i.d(n,"easeOutCubic",function(){return ft}),i.d(n,"easeInOutCubic",function(){return vt}),i.d(n,"easeInQuart",function(){return mt}),i.d(n,"easeOutQuart",function(){return gt}),i.d(n,"easeInOutQuart",function(){return yt}),i.d(n,"easeInQuint",function(){return bt}),i.d(n,"easeOutQuint",function(){return St}),i.d(n,"easeInOutQuint",function(){return xt});var r={};i.r(r),i.d(r,"VApp",function(){return f}),i.d(r,"VAppBar",function(){return Ii}),i.d(r,"VAppBarNavIcon",function(){return hn}),i.d(r,"VAppBarTitle",function(){return dn}),i.d(r,"VAlert",function(){return mn}),i.d(r,"VAutocomplete",function(){return Hs}),i.d(r,"VAvatar",function(){return Zr}),i.d(r,"VBadge",function(){return Ws}),i.d(r,"VBanner",function(){return Gs}),i.d(r,"VBottomNavigation",function(){return Xs}),i.d(r,"VBottomSheet",function(){return so}),i.d(r,"VBreadcrumbs",function(){return co}),i.d(r,"VBreadcrumbsItem",function(){return ao}),i.d(r,"VBreadcrumbsDivider",function(){return lo}),i.d(r,"VBtn",function(){return un}),i.d(r,"VBtnToggle",function(){return po}),i.d(r,"VCalendar",function(){return ll}),i.d(r,"VCalendarCategory",function(){return sl}),i.d(r,"VCalendarDaily",function(){return Ka}),i.d(r,"VCalendarWeekly",function(){return Ya}),i.d(r,"VCalendarMonthly",function(){return Ga}),i.d(r,"VCard",function(){return cl}),i.d(r,"VCardActions",function(){return hl}),i.d(r,"VCardSubtitle",function(){return dl}),i.d(r,"VCardText",function(){return pl}),i.d(r,"VCardTitle",function(){return fl}),i.d(r,"VCarousel",function(){return wl}),i.d(r,"VCarouselItem",function(){return $l}),i.d(r,"VCheckbox",function(){return Bl}),i.d(r,"VSimpleCheckbox",function(){return Cr}),i.d(r,"VChip",function(){return Gn}),i.d(r,"VChipGroup",function(){return Vl}),i.d(r,"VColorPicker",function(){return Iu}),i.d(r,"VColorPickerSwatches",function(){return ku}),i.d(r,"VColorPickerCanvas",function(){return Kl}),i.d(r,"VContent",function(){return _u}),i.d(r,"VCombobox",function(){return Bu}),i.d(r,"VCounter",function(){return gs}),i.d(r,"VData",function(){return Mu}),i.d(r,"VDataIterator",function(){return Hu}),i.d(r,"VDataFooter",function(){return Lu}),i.d(r,"VDataTable",function(){return lc}),i.d(r,"VEditDialog",function(){return uc}),i.d(r,"VTableOverflow",function(){return hc}),i.d(r,"VDataTableHeader",function(){return Xu}),i.d(r,"VSimpleTable",function(){return ec}),i.d(r,"VVirtualTable",function(){return cc}),i.d(r,"VDatePicker",function(){return Hc}),i.d(r,"VDatePickerTitle",function(){return pc}),i.d(r,"VDatePickerHeader",function(){return xc}),i.d(r,"VDatePickerDateTable",function(){return Ac}),i.d(r,"VDatePickerMonthTable",function(){return Ec}),i.d(r,"VDatePickerYears",function(){return Dc}),i.d(r,"VDialog",function(){return no}),i.d(r,"VDivider",function(){return $r}),i.d(r,"VExpansionPanels",function(){return Nc}),i.d(r,"VExpansionPanel",function(){return Wc}),i.d(r,"VExpansionPanelHeader",function(){return Gc}),i.d(r,"VExpansionPanelContent",function(){return Rc}),i.d(r,"VFileInput",function(){return Jc}),i.d(r,"VFooter",function(){return th}),i.d(r,"VForm",function(){return ih}),i.d(r,"VContainer",function(){return rh}),i.d(r,"VCol",function(){return ph}),i.d(r,"VRow",function(){return Bh}),i.d(r,"VSpacer",function(){return Ah}),i.d(r,"VLayout",function(){return Eh}),i.d(r,"VFlex",function(){return Dh}),i.d(r,"VHover",function(){return Mh}),i.d(r,"VIcon",function(){return Bi}),i.d(r,"VImg",function(){return ui}),i.d(r,"VInput",function(){return fs}),i.d(r,"VItem",function(){return Lh}),i.d(r,"VItemGroup",function(){return Gr}),i.d(r,"VLabel",function(){return as}),i.d(r,"VLazy",function(){return jh}),i.d(r,"VListItemActionText",function(){return ts}),i.d(r,"VListItemContent",function(){return es}),i.d(r,"VListItemTitle",function(){return is}),i.d(r,"VListItemSubtitle",function(){return ns}),i.d(r,"VList",function(){return Vr}),i.d(r,"VListGroup",function(){return jr}),i.d(r,"VListItem",function(){return Ar}),i.d(r,"VListItemAction",function(){return Er}),i.d(r,"VListItemAvatar",function(){return Qr}),i.d(r,"VListItemIcon",function(){return Lr}),i.d(r,"VListItemGroup",function(){return qr}),i.d(r,"VMain",function(){return Ou}),i.d(r,"VMenu",function(){return Sr}),i.d(r,"VMessages",function(){return us}),i.d(r,"VNavigationDrawer",function(){return Fh}),i.d(r,"VOverflowBtn",function(){return zh}),i.d(r,"VOverlay",function(){return Ks}),i.d(r,"VPagination",function(){return Gh}),i.d(r,"VSheet",function(){return Ye}),i.d(r,"VParallax",function(){return Uh}),i.d(r,"VPicker",function(){return Vc}),i.d(r,"VProgressCircular",function(){return Di}),i.d(r,"VProgressLinear",function(){return xs}),i.d(r,"VRadioGroup",function(){return Xh}),i.d(r,"VRadio",function(){return Kh}),i.d(r,"VRangeSlider",function(){return id}),i.d(r,"VRating",function(){return nd}),i.d(r,"VResponsive",function(){return Ze}),i.d(r,"VSelect",function(){return Ls}),i.d(r,"VSkeletonLoader",function(){return od}),i.d(r,"VSlider",function(){return Pl}),i.d(r,"VSlideGroup",function(){return Dl}),i.d(r,"VSlideItem",function(){return ad}),i.d(r,"VSnackbar",function(){return ld}),i.d(r,"VSparkline",function(){return Sd}),i.d(r,"VSpeedDial",function(){return xd}),i.d(r,"VStepper",function(){return Cd}),i.d(r,"VStepperContent",function(){return $d}),i.d(r,"VStepperStep",function(){return kd}),i.d(r,"VStepperHeader",function(){return Id}),i.d(r,"VStepperItems",function(){return Od}),i.d(r,"VSubheader",function(){return _r}),i.d(r,"VSwitch",function(){return Td}),i.d(r,"VSystemBar",function(){return Ad}),i.d(r,"VTabs",function(){return Hd}),i.d(r,"VTab",function(){return Nd}),i.d(r,"VTabItem",function(){return zd}),i.d(r,"VTabsItems",function(){return Ld}),i.d(r,"VTabsSlider",function(){return Pd}),i.d(r,"VTextarea",function(){return Rd}),i.d(r,"VTextField",function(){return Bs}),i.d(r,"VThemeProvider",function(){return qn}),i.d(r,"VTimeline",function(){return Gd}),i.d(r,"VTimelineItem",function(){return qd}),i.d(r,"VTimePicker",function(){return rp}),i.d(r,"VTimePickerClock",function(){return Kd}),i.d(r,"VTimePickerTitle",function(){return Xd}),i.d(r,"VToolbar",function(){return di}),i.d(r,"VToolbarItems",function(){return op}),i.d(r,"VToolbarTitle",function(){return sp}),i.d(r,"VTooltip",function(){return ap}),i.d(r,"VTreeview",function(){return xp}),i.d(r,"VTreeviewNode",function(){return fp}),i.d(r,"VVirtualScroll",function(){return wp}),i.d(r,"VWindow",function(){return Sl}),i.d(r,"VWindowItem",function(){return Cl}),i.d(r,"VCarouselTransition",function(){return Cn}),i.d(r,"VCarouselReverseTransition",function(){return kn}),i.d(r,"VTabTransition",function(){return $n}),i.d(r,"VTabReverseTransition",function(){return In}),i.d(r,"VMenuTransition",function(){return On}),i.d(r,"VFabTransition",function(){return _n}),i.d(r,"VDialogTransition",function(){return Tn}),i.d(r,"VDialogBottomTransition",function(){return Bn}),i.d(r,"VDialogTopTransition",function(){return An}),i.d(r,"VFadeTransition",function(){return En}),i.d(r,"VScaleTransition",function(){return Dn}),i.d(r,"VScrollXTransition",function(){return Mn}),i.d(r,"VScrollXReverseTransition",function(){return Vn}),i.d(r,"VScrollYTransition",function(){return Ln}),i.d(r,"VScrollYReverseTransition",function(){return Pn}),i.d(r,"VSlideXTransition",function(){return jn}),i.d(r,"VSlideXReverseTransition",function(){return Hn}),i.d(r,"VSlideYTransition",function(){return Fn}),i.d(r,"VSlideYReverseTransition",function(){return Nn}),i.d(r,"VExpandTransition",function(){return zn}),i.d(r,"VExpandXTransition",function(){return Wn});var s={};i.r(s),i.d(s,"ClickOutside",function(){return pr}),i.d(s,"Intersect",function(){return qe}),i.d(s,"Mutate",function(){return Ip}),i.d(s,"Resize",function(){return vr}),i.d(s,"Ripple",function(){return en}),i.d(s,"Scroll",function(){return fi}),i.d(s,"Touch",function(){return gl});i(10);var o=i(0),a=i.n(o),l=function(){return(l=Object.assign||function(t){for(var e,i=1,n=arguments.length;i0)&&!(n=s.next()).done;)o.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(i=s.return)&&i.call(s)}finally{if(r)throw r.error}}return o},x=function(){for(var t=[],e=0;e":">"};function D(t){return t.replace(/[&<>]/g,function(t){return E[t]||t})}function M(t,e){for(var i={},n=0;n=i&&r=this.thresholds.lg-this.scrollBarWidth;switch(this.height=e,this.width=i,this.xs=n,this.sm=r,this.md=s,this.lg=o,this.xl=a,this.xsOnly=n,this.smOnly=r,this.smAndDown=(n||r)&&!(s||o||a),this.smAndUp=!n&&(r||s||o||a),this.mdOnly=s,this.mdAndDown=(n||r||s)&&!(o||a),this.mdAndUp=!(n||r)&&(s||o||a),this.lgOnly=o,this.lgAndDown=(n||r||s||o)&&!a,this.lgAndUp=!(n||r||s)&&(o||a),this.xlOnly=a,!0){case n:this.name="xs";break;case r:this.name="sm";break;case s:this.name="md";break;case o:this.name="lg";break;default:this.name="xl"}if("number"!=typeof this.mobileBreakpoint){var l={xs:0,sm:1,md:2,lg:3,xl:4},u=l[this.name],c=l[this.mobileBreakpoint];this.mobile=u<=c}else this.mobile=i0)&&!(n=s.next()).done;)o.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(i=s.return)&&i.call(s)}finally{if(r)throw r.error}}return o},Ft=function(){for(var t=[],e=0;e>16&255)/255),s=i((t>>8&255)/255),o=i((t>>0&255)/255),a=0;a<3;++a)e[a]=n[a][0]*r+n[a][1]*s+n[a][2]*o;return e}var Zt=function(){return(Zt=Object.assign||function(t){for(var e,i=1,n=arguments.length;i0)&&!(n=s.next()).done;)o.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(i=s.return)&&i.call(s)}finally{if(r)throw r.error}}return o};function Jt(t){return!!t&&!!t.match(/^(#|var\(--|(rgb|hsl)a?\()/)}function Qt(t){var e;if("number"==typeof t)e=t;else{if("string"!=typeof t)throw new TypeError("Colors can only be numbers or strings, recieved "+(null==t?t:t.constructor.name)+" instead");var i="#"===t[0]?t.substring(1):t;3===i.length&&(i=i.split("").map(function(t){return t+t}).join("")),6!==i.length&&Te("'"+t+"' is not a valid rgb color"),e=parseInt(i,16)}return e<0?(Te("Colors cannot be negative: '"+t+"'"),e=0):(e>16777215||isNaN(e))&&(Te("'"+t+"' is not a valid rgb color"),e=16777215),e}function te(t){var e=t.toString(16);return e.length<6&&(e="0".repeat(6-e.length)+e),"#"+e}function ee(t){var e=t.h,i=t.s,n=t.v,r=t.a,s=function(t){var r=(t+e/60)%6;return n-n*i*Math.max(Math.min(r,4-r,1),0)},o=[s(5),s(3),s(1)].map(function(t){return Math.round(255*t)});return{r:o[0],g:o[1],b:o[2],a:r}}function ie(t){if(!t)return{h:0,s:1,v:1,a:1};var e=t.r/255,i=t.g/255,n=t.b/255,r=Math.max(e,i,n),s=Math.min(e,i,n),o=0;r!==s&&(r===e?o=60*(0+(i-n)/(r-s)):r===i?o=60*(2+(n-e)/(r-s)):r===n&&(o=60*(4+(e-i)/(r-s)))),o<0&&(o+=360);var a=[o,0===r?0:(r-s)/r,r];return{h:a[0],s:a[1],v:a[2],a:t.a}}function ne(t){var e=t.h,i=t.s,n=t.v,r=t.a,s=n-n*i/2;return{h:e,s:1===s||0===s?0:(n-s)/Math.min(s,1-s),l:s,a:r}}function re(t){return"rgba("+t.r+", "+t.g+", "+t.b+", "+t.a+")"}function se(t){var e=function(t){var e=Math.round(t).toString(16);return("00".substr(0,2-e.length)+e).toUpperCase()};return"#"+[e(t.r),e(t.g),e(t.b),e(Math.round(255*t.a))].join("")}function oe(t){var e=function(t,e){void 0===e&&(e=1);for(var i=[],n=0;nMath.pow(.20689655172413793,3)?Math.cbrt(t):t/(3*Math.pow(.20689655172413793,2))+4/29},de=function(t){return t>.20689655172413793?Math.pow(t,3):3*Math.pow(.20689655172413793,2)*(t-4/29)};function pe(t){var e=he,i=e(t[1]);return[116*i-16,500*(e(t[0]/.95047)-i),200*(i-e(t[2]/1.08883))]}function fe(t){var e=de,i=(t[0]+16)/116;return[.95047*e(i+t[1]/500),e(i),1.08883*e(i-t[2]/200)]}function ve(t){return(ve="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}var me=function(t,e){var i={};for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&e.indexOf(n)<0&&(i[n]=t[n]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols){var r=0;for(n=Object.getOwnPropertySymbols(t);r0)&&!(n=s.next()).done;)o.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(i=s.return)&&i.call(s)}finally{if(r)throw r.error}}return o};var ye=function(t,e){return"\n.v-application ."+t+" {\n background-color: "+e+" !important;\n border-color: "+e+" !important;\n}\n.v-application ."+t+"--text {\n color: "+e+" !important;\n caret-color: "+e+" !important;\n}"},be=function(t,e,i){var n=ge(e.split(/(\d)/,2),2),r=n[0],s=n[1];return"\n.v-application ."+t+"."+r+"-"+s+" {\n background-color: "+i+" !important;\n border-color: "+i+" !important;\n}\n.v-application ."+t+"--text.text--"+r+"-"+s+" {\n color: "+i+" !important;\n caret-color: "+i+" !important;\n}"},Se=function(t,e){return void 0===e&&(e="base"),"--v-"+t+"-"+e},xe=function(t,e){return void 0===e&&(e="base"),"var("+Se(t,e)+")"};function we(t,e){for(var i={base:te(e)},n=5;n>0;--n)i["lighten"+n]=te(Ce(e,n));for(n=1;n<=4;++n)i["darken"+n]=te(ke(e,n));return i}function Ce(t,e){var i=pe(Xt(t));return i[0]=i[0]+10*e,qt(fe(i))}function ke(t,e){var i=pe(Xt(t));return i[0]=i[0]-10*e,qt(fe(i))}var $e=function(){var t=function(e,i){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i])})(e,i)};return function(e,i){function n(){this.constructor=e}t(e,i),e.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}}(),Ie=function(t){function e(i){var n=t.call(this)||this;n.disabled=!1,n.isDark=null,n.unwatch=null,n.vueMeta=null;var r=i[e.property],s=r.dark,o=r.disable,a=r.options,l=r.themes;return n.dark=Boolean(s),n.defaults=n.themes=l,n.options=a,o?(n.disabled=!0,n):(n.themes={dark:n.fillVariant(l.dark,!0),light:n.fillVariant(l.light,!1)},n)}return $e(e,t),Object.defineProperty(e.prototype,"css",{set:function(t){this.vueMeta?this.isVueMeta23&&this.applyVueMeta23():this.checkOrCreateStyleElement()&&(this.styleEl.innerHTML=t)},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"dark",{get:function(){return Boolean(this.isDark)},set:function(t){var e=this.isDark;this.isDark=t,null!=e&&this.applyTheme()},enumerable:!1,configurable:!0}),e.prototype.applyTheme=function(){if(this.disabled)return this.clearCss();this.css=this.generatedStyles},e.prototype.clearCss=function(){this.css=""},e.prototype.init=function(t,e){this.disabled||(t.$meta?this.initVueMeta(t):e&&this.initSSR(e),this.initTheme(t))},e.prototype.setTheme=function(t,e){this.themes[t]=Object.assign(this.themes[t],e),this.applyTheme()},e.prototype.resetThemes=function(){this.themes.light=Object.assign({},this.defaults.light),this.themes.dark=Object.assign({},this.defaults.dark),this.applyTheme()},e.prototype.checkOrCreateStyleElement=function(){return this.styleEl=document.getElementById("vuetify-theme-stylesheet"),!!this.styleEl||(this.genStyleElement(),Boolean(this.styleEl))},e.prototype.fillVariant=function(t,e){void 0===t&&(t={});var i=this.themes[e?"dark":"light"];return Object.assign({},i,t)},e.prototype.genStyleElement=function(){"undefined"!=typeof document&&(this.styleEl=document.createElement("style"),this.styleEl.type="text/css",this.styleEl.id="vuetify-theme-stylesheet",this.options.cspNonce&&this.styleEl.setAttribute("nonce",this.options.cspNonce),document.head.appendChild(this.styleEl))},e.prototype.initVueMeta=function(t){var e=this;if(this.vueMeta=t.$meta(),this.isVueMeta23)t.$nextTick(function(){e.applyVueMeta23()});else{var i="function"==typeof this.vueMeta.getOptions?this.vueMeta.getOptions().keyName:"metaInfo",n=t.$options[i]||{};t.$options[i]=function(){n.style=n.style||[];var t=n.style.find(function(t){return"vuetify-theme-stylesheet"===t.id});return t?t.cssText=e.generatedStyles:n.style.push({cssText:e.generatedStyles,type:"text/css",id:"vuetify-theme-stylesheet",nonce:(e.options||{}).cspNonce}),n}}},e.prototype.applyVueMeta23=function(){(0,this.vueMeta.addApp("vuetify").set)({style:[{cssText:this.generatedStyles,type:"text/css",id:"vuetify-theme-stylesheet",nonce:this.options.cspNonce}]})},e.prototype.initSSR=function(t){var e=this.options.cspNonce?' nonce="'+this.options.cspNonce+'"':"";t.head=t.head||"",t.head+='"},e.prototype.initTheme=function(t){var e=this;"undefined"!=typeof document&&(this.unwatch&&(this.unwatch(),this.unwatch=null),t.$once("hook:created",function(){var i=a.a.observable({themes:e.themes});e.unwatch=t.$watch(function(){return i.themes},function(){return e.applyTheme()},{deep:!0})}),this.applyTheme())},Object.defineProperty(e.prototype,"currentTheme",{get:function(){var t=this.dark?"dark":"light";return this.themes[t]},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"generatedStyles",{get:function(){var t,e=this.parsedTheme,i=this.options||{};return null!=i.themeCache&&null!=(t=i.themeCache.get(e))?t:(t=function(t,e){void 0===e&&(e=!1);var i=t.anchor,n=me(t,["anchor"]),r=Object.keys(n);if(!r.length)return"";var s="",o="";o+=".v-application a { color: "+(e?xe("anchor"):i)+"; }",e&&(s+=" "+Se("anchor")+": "+i+";\n");for(var a=0;a0){var n=e[e.length-1];if(n.constructor===t.constructor){i++,t=t.$parent;continue}i>0&&(e[e.length-1]=[n,i],i=0)}e.push(t),t=t.$parent}return"\n\nfound in\n\n"+e.map(function(t,e){return""+(0===e?"---\x3e ":" ".repeat(5+2*e))+(Array.isArray(t)?Le(t[0])+"... ("+t[1]+" recursive calls)":Le(t))}).join("\n")}return"\n\n(found in "+Le(t)+")"}(e):"")}}function Te(t,e,i){var n=_e(t,e,i);null!=n&&console.warn(n)}function Be(t,e,i){var n=_e(t,e,i);null!=n&&console.error(n)}function Ae(t,e,i,n){Te("[UPGRADE] '"+t+"' is deprecated, use '"+e+"' instead.",i,n)}function Ee(t,e,i,n){Be("[BREAKING] '"+t+"' has been removed, use '"+e+"' instead. For more information, see the upgrade guide https://github.com/vuetifyjs/vuetify/releases/tag/v2.0.0#user-content-upgrade-guide",i,n)}function De(t,e,i){Te("[REMOVED] '"+t+"' has been removed. You can safely omit it.",e,i)}var Me=/(?:^|[-_])(\w)/g,Ve=function(t){return t.replace(Me,function(t){return t.toUpperCase()}).replace(/[-_]/g,"")};function Le(t,e){if(t.$root===t)return"";var i="function"==typeof t&&null!=t.cid?t.options:t._isVue?t.$options||t.constructor.options:t||{},n=i.name||i._componentTag,r=i.__file;if(!n&&r){var s=r.match(/([^/\\]+)\.vue$/);n=s&&s[1]}return(n?"<"+Ve(n)+">":"")+(r&&!1!==e?" at "+r:"")}var Pe=function(){return(Pe=Object.assign||function(t){for(var e,i=1,n=arguments.length;i0)&&!(n=s.next()).done;)o.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(i=s.return)&&i.call(s)}finally{if(r)throw r.error}}return o},He=a.a.extend({name:"colorable",props:{color:String},methods:{setBackgroundColor:function(t,e){var i;return void 0===e&&(e={}),"string"==typeof e.style?(Be("style must be an object",this),e):"string"==typeof e.class?(Be("class must be an object",this),e):(Jt(t)?e.style=Pe(Pe({},e.style),{"background-color":""+t,"border-color":""+t}):t&&(e.class=Pe(Pe({},e.class),((i={})[t]=!0,i))),e)},setTextColor:function(t,e){var i;if(void 0===e&&(e={}),"string"==typeof e.style)return Be("style must be an object",this),e;if("string"==typeof e.class)return Be("class must be an object",this),e;if(Jt(t))e.style=Pe(Pe({},e.style),{color:""+t,"caret-color":""+t});else if(t){var n=je(t.toString().trim().split(" ",2),2),r=n[0],s=n[1];e.class=Pe(Pe({},e.class),((i={})[r+"--text"]=!0,i)),s&&(e.class["text--"+s]=!0)}return e}}}),Fe=a.a.extend({name:"elevatable",props:{elevation:[Number,String]},computed:{computedElevation:function(){return this.elevation},elevationClasses:function(){var t,e=this.computedElevation;return null==e?{}:isNaN(parseInt(e))?{}:((t={})["elevation-"+this.elevation]=!0,t)}}}),Ne=a.a.extend({name:"measurable",props:{height:[Number,String],maxHeight:[Number,String],maxWidth:[Number,String],minHeight:[Number,String],minWidth:[Number,String],width:[Number,String]},computed:{measurableStyles:function(){var t={},e=V(this.height),i=V(this.minHeight),n=V(this.minWidth),r=V(this.maxHeight),s=V(this.maxWidth),o=V(this.width);return e&&(t.height=e),i&&(t.minHeight=i),n&&(t.minWidth=n),r&&(t.maxHeight=r),s&&(t.maxWidth=s),o&&(t.width=o),t}}}),ze=function(t){var e="function"==typeof Symbol&&Symbol.iterator,i=e&&t[e],n=0;if(i)return i.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},We=a.a.extend({name:"roundable",props:{rounded:[Boolean,String],tile:Boolean},computed:{roundedClasses:function(){var t,e,i,n=[],r="string"==typeof this.rounded?String(this.rounded):!0===this.rounded;if(this.tile)n.push("rounded-0");else if("string"==typeof r){var s=r.split(" ");try{for(var o=ze(s),a=o.next();!a.done;a=o.next()){var l=a.value;n.push("rounded-"+l)}}catch(e){t={error:e}}finally{try{a&&!a.done&&(e=o.return)&&e.call(o)}finally{if(t)throw t.error}}}else r&&n.push("rounded");return n.length>0?((i={})[n.join(" ")]=!0,i):{}}}}),Re=function(){return(Re=Object.assign||function(t){for(var e,i=1,n=arguments.length;i=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},ti=function(t,e){var i="function"==typeof Symbol&&t[Symbol.iterator];if(!i)return t;var n,r,s=i.call(t),o=[];try{for(;(void 0===e||e-- >0)&&!(n=s.next()).done;)o.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(i=s.return)&&i.call(s)}finally{if(r)throw r.error}}return o},ei={styleList:/;(?![^(]*\))/g,styleProp:/:(.*)/};function ii(t){var e,i,n={};try{for(var r=Qe(t.split(ei.styleList)),s=r.next();!s.done;s=r.next()){var o=s.value,a=ti(o.split(ei.styleProp),2),l=a[0],u=a[1];(l=l.trim())&&("string"==typeof u&&(u=u.trim()),n[N(l)]=u)}}catch(t){e={error:t}}finally{try{s&&!s.done&&(i=r.return)&&i.call(r)}finally{if(e)throw e.error}}return n}function ni(){for(var t,e,i,n={},r=arguments.length;r--;)try{for(var s=(t=void 0,Qe(Object.keys(arguments[r]))),o=s.next();!o.done;o=s.next())switch(i=o.value){case"class":case"directives":arguments[r][i]&&(n[i]=si(n[i],arguments[r][i]));break;case"style":arguments[r][i]&&(n[i]=ri(n[i],arguments[r][i]));break;case"staticClass":if(!arguments[r][i])break;void 0===n[i]&&(n[i]=""),n[i]&&(n[i]+=" "),n[i]+=arguments[r][i].trim();break;case"on":case"nativeOn":arguments[r][i]&&(n[i]=oi(n[i],arguments[r][i]));break;case"attrs":case"props":case"domProps":case"scopedSlots":case"staticStyle":case"hook":case"transition":if(!arguments[r][i])break;n[i]||(n[i]={}),n[i]=Je(Je({},arguments[r][i]),n[i]);break;default:n[i]||(n[i]=arguments[r][i])}}catch(e){t={error:e}}finally{try{o&&!o.done&&(e=s.return)&&e.call(s)}finally{if(t)throw t.error}}return n}function ri(t,e){return t?e?(t=W("string"==typeof t?ii(t):t)).concat("string"==typeof e?ii(e):e):t:e}function si(t,e){return e?t&&t?W(t).concat(e):e:t}function oi(){for(var t=[],e=0;e0)&&!(n=s.next()).done;)o.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(i=s.return)&&i.call(s)}finally{if(r)throw r.error}}return o},di=Ye.extend({name:"v-toolbar",props:{absolute:Boolean,bottom:Boolean,collapse:Boolean,dense:Boolean,extended:Boolean,extensionHeight:{default:48,type:[Number,String]},flat:Boolean,floating:Boolean,prominent:Boolean,short:Boolean,src:{type:[String,Object],default:""},tag:{type:String,default:"header"}},data:function(){return{isExtended:!1}},computed:{computedHeight:function(){var t=this.computedContentHeight;if(!this.isExtended)return t;var e=parseInt(this.extensionHeight);return this.isCollapsed?t:t+(isNaN(e)?0:e)},computedContentHeight:function(){return this.height?parseInt(this.height):this.isProminent&&this.dense?96:this.isProminent&&this.short?112:this.isProminent?128:this.dense?48:this.short||this.$vuetify.breakpoint.smAndDown?56:64},classes:function(){return ci(ci({},Ye.options.computed.classes.call(this)),{"v-toolbar":!0,"v-toolbar--absolute":this.absolute,"v-toolbar--bottom":this.bottom,"v-toolbar--collapse":this.collapse,"v-toolbar--collapsed":this.isCollapsed,"v-toolbar--dense":this.dense,"v-toolbar--extended":this.isExtended,"v-toolbar--flat":this.flat,"v-toolbar--floating":this.floating,"v-toolbar--prominent":this.isProminent})},isCollapsed:function(){return this.collapse},isProminent:function(){return this.prominent},styles:function(){return ci(ci({},this.measurableStyles),{height:V(this.computedHeight)})}},created:function(){var t=this;[["app",""],["manual-scroll",''],["clipped-left",""],["clipped-right",""],["inverted-scroll",""],["scroll-off-screen",""],["scroll-target",""],["scroll-threshold",""],["card",""]].forEach(function(e){var i=hi(e,2),n=i[0],r=i[1];t.$attrs.hasOwnProperty(n)&&Ee(n,r,t)})},methods:{genBackground:function(){var t={height:V(this.computedHeight),src:this.src},e=this.$scopedSlots.img?this.$scopedSlots.img({props:t}):this.$createElement(ui,{props:t});return this.$createElement("div",{staticClass:"v-toolbar__image"},[e])},genContent:function(){return this.$createElement("div",{staticClass:"v-toolbar__content",style:{height:V(this.computedContentHeight)}},U(this))},genExtension:function(){return this.$createElement("div",{staticClass:"v-toolbar__extension",style:{height:V(this.extensionHeight)}},U(this,"extension"))}},render:function(t){this.isExtended=this.extended||!!this.$scopedSlots.extension;var e=[this.genContent()],i=this.setBackgroundColor(this.color,{class:this.classes,style:this.styles,on:this.$listeners});return this.isExtended&&e.push(this.genExtension()),(this.src||this.$scopedSlots.img)&&e.unshift(this.genBackground()),t(this.tag,i,e)}});function pi(t){return(pi="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}var fi={inserted:function(t,e){var i=(e.modifiers||{}).self,n=void 0!==i&&i,r=e.value,s="object"===pi(r)&&r.options||{passive:!0},o="function"==typeof r||"handleEvent"in r?r:r.handler,a=n?t:e.arg?document.querySelector(e.arg):window;a&&(a.addEventListener("scroll",o,s),t._onScroll={handler:o,options:s,target:n?void 0:a})},unbind:function(t){if(t._onScroll){var e=t._onScroll,i=e.handler,n=e.options,r=e.target;(void 0===r?t:r).removeEventListener("scroll",i,n),delete t._onScroll}}},vi=fi,mi={absolute:Boolean,bottom:Boolean,fixed:Boolean,left:Boolean,right:Boolean,top:Boolean};function gi(t){return void 0===t&&(t=[]),a.a.extend({name:"positionable",props:t.length?M(mi,t):mi})}var yi=gi();function bi(t,e){return void 0===e&&(e=[]),d(gi(["absolute","fixed"])).extend({name:"applicationable",props:{app:Boolean},computed:{applicationProperty:function(){return t}},watch:{app:function(t,e){e?this.removeApplication(!0):this.callUpdate()},applicationProperty:function(t,e){this.$vuetify.application.unregister(this._uid,e)}},activated:function(){this.callUpdate()},created:function(){for(var t=0,i=e.length;tt.computedScrollThreshold&&t.thresholdMet()}))},thresholdMet:function(){}}}),xi=a.a.extend({name:"ssr-bootable",data:function(){return{isBooted:!1}},mounted:function(){var t=this;window.requestAnimationFrame(function(){t.$el.setAttribute("data-booted","true"),t.isBooted=!0})}});function wi(t,e){var i,n;return void 0===t&&(t="value"),void 0===e&&(e="input"),a.a.extend({name:"toggleable",model:{prop:t,event:e},props:(i={},i[t]={required:!1},i),data:function(){return{isActive:!!this[t]}},watch:(n={},n[t]=function(t){this.isActive=!!t},n.isActive=function(i){!!i!==this[t]&&this.$emit(e,i)},n)})}var Ci,ki=wi(),$i=function(){return($i=Object.assign||function(t){for(var e,i=1,n=arguments.length;i0,"v-app-bar--shrink-on-scroll":this.shrinkOnScroll})},scrollRatio:function(){var t=this.computedScrollThreshold;return Math.max((t-this.currentScroll)/t,0)},computedContentHeight:function(){if(!this.shrinkOnScroll)return di.options.computed.computedContentHeight.call(this);var t=this.dense?48:56;return t+(this.computedOriginalHeight-t)*this.scrollRatio},computedFontSize:function(){if(this.isProminent){return 1.25+.25*this.scrollRatio}},computedLeft:function(){return!this.app||this.clippedLeft?0:this.$vuetify.application.left},computedMarginTop:function(){return this.app?this.$vuetify.application.bar:0},computedOpacity:function(){if(this.fadeImgOnScroll)return this.scrollRatio},computedOriginalHeight:function(){var t=di.options.computed.computedContentHeight.call(this);return this.isExtended&&(t+=parseInt(this.extensionHeight)),t},computedRight:function(){return!this.app||this.clippedRight?0:this.$vuetify.application.right},computedScrollThreshold:function(){return this.scrollThreshold?Number(this.scrollThreshold):this.computedOriginalHeight-(this.dense?48:56)},computedTransform:function(){if(!this.canScroll||this.elevateOnScroll&&0===this.currentScroll&&this.isActive)return 0;if(this.isActive)return 0;var t=this.scrollOffScreen?this.computedHeight:this.computedContentHeight;return this.bottom?t:-t},hideShadow:function(){return this.elevateOnScroll&&this.isExtended?this.currentScroll0:di.options.computed.isCollapsed.call(this)},isProminent:function(){return di.options.computed.isProminent.call(this)||this.shrinkOnScroll},styles:function(){return $i($i({},di.options.computed.styles.call(this)),{fontSize:V(this.computedFontSize,"rem"),marginTop:V(this.computedMarginTop),transform:"translateY("+V(this.computedTransform)+")",left:V(this.computedLeft),right:V(this.computedRight)})}},watch:{canScroll:"onScroll",computedTransform:function(){this.canScroll&&(this.clippedLeft||this.clippedRight)&&this.callUpdate()},invertedScroll:function(t){this.isActive=!t||0!==this.currentScroll}},created:function(){this.invertedScroll&&(this.isActive=!1)},methods:{genBackground:function(){var t=di.options.methods.genBackground.call(this);return t.data=this._b(t.data||{},t.tag,{style:{opacity:this.computedOpacity}}),t},updateApplication:function(){return this.invertedScroll?0:this.computedHeight+this.computedTransform},thresholdMet:function(){this.invertedScroll?this.isActive=this.currentScroll>this.computedScrollThreshold:(this.hideOnScroll&&(this.isActive=this.isScrollingUp||this.currentScroll4}(e)?this.renderSvgIcon(e,t):this.renderFontIcon(e,t):this.renderSvgIconComponent(e,t)}}),Bi=a.a.extend({name:"v-icon",$_wrapperFor:Ti,functional:!0,render:function(t,e){var i=e.data,n=e.children,r="";return i.domProps&&(r=i.domProps.textContent||i.domProps.innerHTML||r,delete i.domProps.textContent,delete i.domProps.innerHTML),t(Ti,i,r?[r]:n)}}),Ai=Bi,Ei=(i(16),Ye),Di=(i(18),He.extend({name:"v-progress-circular",props:{button:Boolean,indeterminate:Boolean,rotate:{type:[Number,String],default:0},size:{type:[Number,String],default:32},width:{type:[Number,String],default:4},value:{type:[Number,String],default:0}},data:function(){return{radius:20}},computed:{calculatedSize:function(){return Number(this.size)+(this.button?8:0)},circumference:function(){return 2*Math.PI*this.radius},classes:function(){return{"v-progress-circular--indeterminate":this.indeterminate,"v-progress-circular--button":this.button}},normalizedValue:function(){return this.value<0?0:this.value>100?100:parseFloat(this.value)},strokeDashArray:function(){return Math.round(1e3*this.circumference)/1e3},strokeDashOffset:function(){return(100-this.normalizedValue)/100*this.circumference+"px"},strokeWidth:function(){return Number(this.width)/+this.size*this.viewBoxSize*2},styles:function(){return{height:V(this.calculatedSize),width:V(this.calculatedSize)}},svgStyles:function(){return{transform:"rotate("+Number(this.rotate)+"deg)"}},viewBoxSize:function(){return this.radius/(1-Number(this.width)/+this.size)}},methods:{genCircle:function(t,e){return this.$createElement("circle",{class:"v-progress-circular__"+t,attrs:{fill:"transparent",cx:2*this.viewBoxSize,cy:2*this.viewBoxSize,r:this.radius,"stroke-width":this.strokeWidth,"stroke-dasharray":this.strokeDashArray,"stroke-dashoffset":e}})},genSvg:function(){var t=[this.indeterminate||this.genCircle("underlay",0),this.genCircle("overlay",this.strokeDashOffset)];return this.$createElement("svg",{style:this.svgStyles,attrs:{xmlns:"http://www.w3.org/2000/svg",viewBox:this.viewBoxSize+" "+this.viewBoxSize+" "+2*this.viewBoxSize+" "+2*this.viewBoxSize}},t)},genInfo:function(){return this.$createElement("div",{staticClass:"v-progress-circular__info"},this.$slots.default)}},render:function(t){return t("div",this.setTextColor(this.color,{staticClass:"v-progress-circular",attrs:{role:"progressbar","aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":this.indeterminate?void 0:this.normalizedValue},class:this.classes,style:this.styles,on:this.$listeners}),[this.genSvg(),this.genInfo()])}})),Mi=Di;function Vi(t,e){return function(){return Te("The "+t+" component must be used inside a "+e)}}function Li(t,e,i){var n,r=e&&i?{register:Vi(e,i),unregister:Vi(e,i)}:null;return a.a.extend({name:"registrable-inject",inject:(n={},n[t]={default:r},n)})}function Pi(t,e){return void 0===e&&(e=!1),a.a.extend({name:"registrable-provide",provide:function(){var i;return(i={})[t]=e?this:{register:this.register,unregister:this.unregister},i}})}function ji(t,e,i){return Li(t,e,i).extend({name:"groupable",props:{activeClass:{type:String,default:function(){if(this[t])return this[t].activeClass}},disabled:Boolean},data:function(){return{isActive:!1}},computed:{groupClasses:function(){var t;return this.activeClass?((t={})[this.activeClass]=this.isActive,t):{}}},created:function(){this[t]&&this[t].register(this)},beforeDestroy:function(){this[t]&&this[t].unregister(this)},methods:{toggle:function(){this.$emit("change")}}})}ji("itemGroup"),i(17);var Hi=Symbol("rippleStop"),Fi=80;function Ni(t,e){t.style.transform=e,t.style.webkitTransform=e}function zi(t,e){t.style.opacity=e.toString()}function Wi(t){return"TouchEvent"===t.constructor.name}function Ri(t){return"KeyboardEvent"===t.constructor.name}var Yi={show:function(t,e,i){if(void 0===i&&(i={}),e._ripple&&e._ripple.enabled){var n=document.createElement("span"),r=document.createElement("span");n.appendChild(r),n.className="v-ripple__container",i.class&&(n.className+=" "+i.class);var s=function(t,e,i){void 0===i&&(i={});var n=0,r=0;if(!Ri(t)){var s=e.getBoundingClientRect(),o=Wi(t)?t.touches[t.touches.length-1]:t;n=o.clientX-s.left,r=o.clientY-s.top}var a=0,l=.3;e._ripple&&e._ripple.circle?(l=.15,a=e.clientWidth/2,a=i.center?a:a+Math.sqrt(Math.pow(n-a,2)+Math.pow(r-a,2))/4):a=Math.sqrt(Math.pow(e.clientWidth,2)+Math.pow(e.clientHeight,2))/2;var u=(e.clientWidth-2*a)/2+"px",c=(e.clientHeight-2*a)/2+"px";return{radius:a,scale:l,x:i.center?u:n-a+"px",y:i.center?c:r-a+"px",centerX:u,centerY:c}}(t,e,i),o=s.radius,a=s.scale,l=s.x,u=s.y,c=s.centerX,h=s.centerY,d=2*o+"px";r.className="v-ripple__animation",r.style.width=d,r.style.height=d,e.appendChild(n);var p=window.getComputedStyle(e);p&&"static"===p.position&&(e.style.position="relative",e.dataset.previousPosition="static"),r.classList.add("v-ripple__animation--enter"),r.classList.add("v-ripple__animation--visible"),Ni(r,"translate("+l+", "+u+") scale3d("+a+","+a+","+a+")"),zi(r,0),r.dataset.activated=String(performance.now()),setTimeout(function(){r.classList.remove("v-ripple__animation--enter"),r.classList.add("v-ripple__animation--in"),Ni(r,"translate("+c+", "+h+") scale3d(1,1,1)"),zi(r,.25)},0)}},hide:function(t){if(t&&t._ripple&&t._ripple.enabled){var e=t.getElementsByClassName("v-ripple__animation");if(0!==e.length){var i=e[e.length-1];if(!i.dataset.isHiding){i.dataset.isHiding="true";var n=performance.now()-Number(i.dataset.activated),r=Math.max(250-n,0);setTimeout(function(){i.classList.remove("v-ripple__animation--in"),i.classList.add("v-ripple__animation--out"),zi(i,0),setTimeout(function(){1===t.getElementsByClassName("v-ripple__animation").length&&t.dataset.previousPosition&&(t.style.position=t.dataset.previousPosition,delete t.dataset.previousPosition),i.parentNode&&t.removeChild(i.parentNode)},300)},r)}}}}};function Gi(t){return void 0===t||!!t}function Ui(t){var e={},i=t.currentTarget;if(i&&i._ripple&&!i._ripple.touched&&!t[Hi]){if(t[Hi]=!0,Wi(t))i._ripple.touched=!0,i._ripple.isTouch=!0;else if(i._ripple.isTouch)return;if(e.center=i._ripple.centered||Ri(t),i._ripple.class&&(e.class=i._ripple.class),Wi(t)){if(i._ripple.showTimerCommit)return;i._ripple.showTimerCommit=function(){Yi.show(t,i,e)},i._ripple.showTimer=window.setTimeout(function(){i&&i._ripple&&i._ripple.showTimerCommit&&(i._ripple.showTimerCommit(),i._ripple.showTimerCommit=null)},Fi)}else Yi.show(t,i,e)}}function qi(t){var e=t.currentTarget;if(e&&e._ripple){if(window.clearTimeout(e._ripple.showTimer),"touchend"===t.type&&e._ripple.showTimerCommit)return e._ripple.showTimerCommit(),e._ripple.showTimerCommit=null,void(e._ripple.showTimer=setTimeout(function(){qi(t)}));window.setTimeout(function(){e._ripple&&(e._ripple.touched=!1)}),Yi.hide(e)}}function Xi(t){var e=t.currentTarget;e&&e._ripple&&(e._ripple.showTimerCommit&&(e._ripple.showTimerCommit=null),window.clearTimeout(e._ripple.showTimer))}var Zi=!1;function Ki(t){Zi||t.keyCode!==j.enter&&t.keyCode!==j.space||(Zi=!0,Ui(t))}function Ji(t){Zi=!1,qi(t)}function Qi(t,e,i){var n=Gi(e.value);n||Yi.hide(t),t._ripple=t._ripple||{},t._ripple.enabled=n;var r=e.value||{};r.center&&(t._ripple.centered=!0),r.class&&(t._ripple.class=e.value.class),r.circle&&(t._ripple.circle=r.circle),n&&!i?(t.addEventListener("touchstart",Ui,{passive:!0}),t.addEventListener("touchend",qi,{passive:!0}),t.addEventListener("touchmove",Xi,{passive:!0}),t.addEventListener("touchcancel",qi),t.addEventListener("mousedown",Ui),t.addEventListener("mouseup",qi),t.addEventListener("mouseleave",qi),t.addEventListener("keydown",Ki),t.addEventListener("keyup",Ji),t.addEventListener("dragstart",qi,{passive:!0})):!n&&i&&tn(t)}function tn(t){t.removeEventListener("mousedown",Ui),t.removeEventListener("touchstart",Ui),t.removeEventListener("touchend",qi),t.removeEventListener("touchmove",Xi),t.removeEventListener("touchcancel",qi),t.removeEventListener("mouseup",qi),t.removeEventListener("mouseleave",qi),t.removeEventListener("keydown",Ki),t.removeEventListener("keyup",Ji),t.removeEventListener("dragstart",qi)}var en={bind:function(t,e,i){Qi(t,e,!1)},unbind:function(t){delete t._ripple,tn(t)},update:function(t,e){e.value!==e.oldValue&&Qi(t,e,Gi(e.oldValue))}},nn=en,rn=function(){return(rn=Object.assign||function(t){for(var e,i=1,n=arguments.length;i0)&&!(n=s.next()).done;)o.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(i=s.return)&&i.call(s)}finally{if(r)throw r.error}}return o},un=d(Ei,sn,yi,Oi,ji("btnToggle"),wi("inputValue")).extend().extend({name:"v-btn",props:{activeClass:{type:String,default:function(){return this.btnToggle?this.btnToggle.activeClass:""}},block:Boolean,depressed:Boolean,fab:Boolean,icon:Boolean,loading:Boolean,outlined:Boolean,plain:Boolean,retainFocusOnClick:Boolean,rounded:Boolean,tag:{type:String,default:"button"},text:Boolean,tile:Boolean,type:{type:String,default:"button"},value:null},data:function(){return{proxyClass:"v-btn--active"}},computed:{classes:function(){return an(an(an(an(an(an({"v-btn":!0},sn.options.computed.classes.call(this)),{"v-btn--absolute":this.absolute,"v-btn--block":this.block,"v-btn--bottom":this.bottom,"v-btn--disabled":this.disabled,"v-btn--is-elevated":this.isElevated,"v-btn--fab":this.fab,"v-btn--fixed":this.fixed,"v-btn--has-bg":this.hasBg,"v-btn--icon":this.icon,"v-btn--left":this.left,"v-btn--loading":this.loading,"v-btn--outlined":this.outlined,"v-btn--plain":this.plain,"v-btn--right":this.right,"v-btn--round":this.isRound,"v-btn--rounded":this.rounded,"v-btn--router":this.to,"v-btn--text":this.text,"v-btn--tile":this.tile,"v-btn--top":this.top}),this.themeClasses),this.groupClasses),this.elevationClasses),this.sizeableClasses)},computedElevation:function(){if(!this.disabled)return Fe.options.computed.computedElevation.call(this)},computedRipple:function(){var t,e=!this.icon&&!this.fab||{circle:!0};return!this.disabled&&(null!==(t=this.ripple)&&void 0!==t?t:e)},hasBg:function(){return!(this.text||this.plain||this.outlined||this.icon)},isElevated:function(){return Boolean(!(this.icon||this.text||this.outlined||this.depressed||this.disabled||this.plain||!(null==this.elevation||Number(this.elevation)>0)))},isRound:function(){return Boolean(this.icon||this.fab)},styles:function(){return an({},this.measurableStyles)}},created:function(){var t=this;[["flat","text"],["outline","outlined"],["round","rounded"]].forEach(function(e){var i=ln(e,2),n=i[0],r=i[1];t.$attrs.hasOwnProperty(n)&&Ee(n,r,t)})},methods:{click:function(t){!this.retainFocusOnClick&&!this.fab&&t.detail&&this.$el.blur(),this.$emit("click",t),this.btnToggle&&this.toggle()},genContent:function(){return this.$createElement("span",{staticClass:"v-btn__content"},this.$slots.default)},genLoader:function(){return this.$createElement("span",{class:"v-btn__loader"},this.$slots.loader||[this.$createElement(Mi,{props:{indeterminate:!0,size:23,width:2}})])}},render:function(t){var e=[this.genContent(),this.loading&&this.genLoader()],i=this.generateRouteLink(),n=i.tag,r=i.data,s=this.hasBg?this.setBackgroundColor:this.setTextColor;return"button"===n&&(r.attrs.type=this.type,r.attrs.disabled=this.disabled),r.attrs.value=["string","number"].includes(on(this.value))?this.value:JSON.stringify(this.value),t(n,this.disabled?r:s(this.color,r),e)}}),cn=function(){return(cn=Object.assign||function(t){for(var e,i=1,n=arguments.length;i0)&&!(n=s.next()).done;)o.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(i=s.return)&&i.call(s)}finally{if(r)throw r.error}}return o}),yn=function(){for(var t=[],e=0;e0)&&!(n=s.next()).done;)o.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(i=s.return)&&i.call(s)}finally{if(r)throw r.error}}return o},Gn=d(He,Oi,sn,c,ji("chipGroup"),wi("inputValue")).extend({name:"v-chip",props:{active:{type:Boolean,default:!0},activeClass:{type:String,default:function(){return this.chipGroup?this.chipGroup.activeClass:""}},close:Boolean,closeIcon:{type:String,default:"$delete"},closeLabel:{type:String,default:"$vuetify.close"},disabled:Boolean,draggable:Boolean,filter:Boolean,filterIcon:{type:String,default:"$complete"},label:Boolean,link:Boolean,outlined:Boolean,pill:Boolean,tag:{type:String,default:"span"},textColor:String,value:null},data:function(){return{proxyClass:"v-chip--active"}},computed:{classes:function(){return Rn(Rn(Rn(Rn(Rn({"v-chip":!0},sn.options.computed.classes.call(this)),{"v-chip--clickable":this.isClickable,"v-chip--disabled":this.disabled,"v-chip--draggable":this.draggable,"v-chip--label":this.label,"v-chip--link":this.isLink,"v-chip--no-color":!this.color,"v-chip--outlined":this.outlined,"v-chip--pill":this.pill,"v-chip--removable":this.hasClose}),this.themeClasses),this.sizeableClasses),this.groupClasses)},hasClose:function(){return Boolean(this.close)},isClickable:function(){return Boolean(sn.options.computed.isClickable.call(this)||this.chipGroup)}},created:function(){var t=this;[["outline","outlined"],["selected","input-value"],["value","active"],["@input","@active.sync"]].forEach(function(e){var i=Yn(e,2),n=i[0],r=i[1];t.$attrs.hasOwnProperty(n)&&Ee(n,r,t)})},methods:{click:function(t){this.$emit("click",t),this.chipGroup&&this.toggle()},genFilter:function(){var t=[];return this.isActive&&t.push(this.$createElement(Ai,{staticClass:"v-chip__filter",props:{left:!0}},this.filterIcon)),this.$createElement(Wn,t)},genClose:function(){var t=this;return this.$createElement(Ai,{staticClass:"v-chip__close",props:{right:!0,size:18},attrs:{"aria-label":this.$vuetify.lang.t(this.closeLabel)},on:{click:function(e){e.stopPropagation(),e.preventDefault(),t.$emit("click:close"),t.$emit("update:active",!1)}}},this.closeIcon)},genContent:function(){return this.$createElement("span",{staticClass:"v-chip__content"},[this.filter&&this.genFilter(),this.$slots.default,this.hasClose&&this.genClose()])}},render:function(t){var e=[this.genContent()],i=this.generateRouteLink(),n=i.tag,r=i.data;r.attrs=Rn(Rn({},r.attrs),{draggable:this.draggable?"true":void 0,tabindex:this.chipGroup&&!this.disabled?0:r.attrs.tabindex}),r.directives.push({name:"show",value:this.active}),r=this.setBackgroundColor(this.color,r);var s=this.textColor||this.outlined&&this.color;return t(n,this.setTextColor(s,r),e)}}),Un=Gn,qn=(i(37),c.extend({name:"v-theme-provider",props:{root:Boolean},computed:{isDark:function(){return this.root?this.rootIsDark:c.options.computed.isDark.call(this)}},render:function(){return this.$slots.default&&this.$slots.default.find(function(t){return!t.isComment&&" "!==t.text})}})),Xn=a.a.extend().extend({name:"delayable",props:{openDelay:{type:[Number,String],default:0},closeDelay:{type:[Number,String],default:0}},data:function(){return{openTimeout:void 0,closeTimeout:void 0}},methods:{clearDelay:function(){clearTimeout(this.openTimeout),clearTimeout(this.closeTimeout)},runDelay:function(t,e){var i=this;this.clearDelay();var n=parseInt(this[t+"Delay"],10);this[t+"Timeout"]=setTimeout(e||function(){i.isActive={open:!0,close:!1}[t]},n)}}});function Zn(t){return(Zn="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}var Kn=function(t){var e="function"==typeof Symbol&&Symbol.iterator,i=e&&t[e],n=0;if(i)return i.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},Jn=d(Xn,ki).extend({name:"activatable",props:{activator:{default:null,validator:function(t){return["string","object"].includes(Zn(t))}},disabled:Boolean,internalActivator:Boolean,openOnHover:Boolean,openOnFocus:Boolean},data:function(){return{activatorElement:null,activatorNode:[],events:["click","mouseenter","mouseleave","focus"],listeners:{}}},watch:{activator:"resetActivator",openOnFocus:"resetActivator",openOnHover:"resetActivator"},mounted:function(){var t=Y(this,"activator",!0);t&&["v-slot","normal"].includes(t)&&Be('The activator slot must be bound, try \'