From dd4f3f1a9d0f3b421acea373c14ff2e68992ff94 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Mon, 10 May 2021 10:17:44 +0100 Subject: [PATCH 01/69] [Search Source] Exclude metafields from fields request (#99443) * [Search Source] Exclude metafields from fields request * Fix unit test * Adding additional unit test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../search_source/search_source.test.ts | 26 +++++++++++++++-- .../search/search_source/search_source.ts | 29 +++++++++++-------- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index ef3e020747f7a..7c0473077d182 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -27,7 +27,7 @@ const mockSource2 = { excludes: ['bar-*'] }; const indexPattern = ({ title: 'foo', - fields: [{ name: 'foo-bar' }, { name: 'field1' }, { name: 'field2' }], + fields: [{ name: 'foo-bar' }, { name: 'field1' }, { name: 'field2' }, { name: '_id' }], getComputedFields, getSourceFiltering: () => mockSource, } as unknown) as IndexPattern; @@ -68,7 +68,7 @@ describe('SearchSource', () => { beforeEach(() => { const getConfigMock = jest .fn() - .mockImplementation((param) => param === 'metaFields' && ['_type', '_source']) + .mockImplementation((param) => param === 'metaFields' && ['_type', '_source', '_id']) .mockName('getConfig'); mockSearchMethod = jest @@ -458,6 +458,28 @@ describe('SearchSource', () => { expect(request.fields).toEqual([{ field: 'field1' }, { field: 'field2' }]); }); + test('excludes metafields from the request', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: [], + docvalueFields: [], + }), + } as unknown) as IndexPattern); + searchSource.setField('fields', [{ field: '*', include_unmapped: 'true' }]); + + const request = searchSource.getSearchRequestBody(); + expect(request.fields).toEqual([{ field: 'field1' }, { field: 'field2' }]); + + searchSource.setField('fields', ['foo-bar', 'foo--bar', 'field1', 'field2']); + expect(request.fields).toEqual([{ field: 'field1' }, { field: 'field2' }]); + + searchSource.removeField('fields'); + searchSource.setField('fieldsFromSource', ['foo-bar', 'foo--bar', 'field1', 'field2']); + expect(request.fields).toEqual([{ field: 'field1' }, { field: 'field2' }]); + }); + test('returns all scripted fields when one fields entry is *', async () => { searchSource.setField('index', ({ ...indexPattern, diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 5130224329ba2..f35d2d47f1bf4 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -682,6 +682,7 @@ export class SearchSource { searchRequest.body = searchRequest.body || {}; const { body, index, query, filters, highlightAll } = searchRequest; searchRequest.indexType = this.getIndexType(index); + const metaFields = getConfig(UI_SETTINGS.META_FIELDS); // get some special field types from the index pattern const { docvalueFields, scriptFields, storedFields, runtimeFields } = index @@ -712,7 +713,7 @@ export class SearchSource { body._source = sourceFilters; } - const filter = fieldWildcardFilter(body._source.excludes, getConfig(UI_SETTINGS.META_FIELDS)); + const filter = fieldWildcardFilter(body._source.excludes, metaFields); // also apply filters to provided fields & default docvalueFields body.fields = body.fields.filter((fld: SearchFieldValue) => filter(this.getFieldName(fld))); fieldsFromSource = fieldsFromSource.filter((fld: SearchFieldValue) => @@ -793,17 +794,21 @@ export class SearchSource { const field2Name = this.getFieldName(fld2); return field1Name === field2Name; } - ).map((fld: SearchFieldValue) => { - const fieldName = this.getFieldName(fld); - if (Object.keys(docvaluesIndex).includes(fieldName)) { - // either provide the field object from computed docvalues, - // or merge the user-provided field with the one in docvalues - return typeof fld === 'string' - ? docvaluesIndex[fld] - : this.getFieldFromDocValueFieldsOrIndexPattern(docvaluesIndex, fld, index); - } - return fld; - }); + ) + .filter((fld: SearchFieldValue) => { + return !metaFields.includes(this.getFieldName(fld)); + }) + .map((fld: SearchFieldValue) => { + const fieldName = this.getFieldName(fld); + if (Object.keys(docvaluesIndex).includes(fieldName)) { + // either provide the field object from computed docvalues, + // or merge the user-provided field with the one in docvalues + return typeof fld === 'string' + ? docvaluesIndex[fld] + : this.getFieldFromDocValueFieldsOrIndexPattern(docvaluesIndex, fld, index); + } + return fld; + }); } } else { body.fields = filteredDocvalueFields; From 903b8751da09fffe248d737a7d253b63b9c23ba2 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 10 May 2021 05:13:59 -0700 Subject: [PATCH 02/69] Fix yaml settings editor styling (#99558) --- .../applications/fleet/components/settings_flyout/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx index 56f28ada004e2..d741874a7504c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx @@ -42,6 +42,9 @@ import { isDiffPathProtocol } from '../../../../../common/'; import { SettingsConfirmModal } from './confirm_modal'; import type { SettingsConfirmModalProps } from './confirm_modal'; +import 'brace/mode/yaml'; +import 'brace/theme/textmate'; + const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm; interface Props { @@ -323,6 +326,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { maxLines: 30, tabSize: 2, showGutter: false, + showPrintMargin: false, }} {...inputs.additionalYamlConfig.props} onChange={inputs.additionalYamlConfig.setValue} From ff9cc4c36245b9951528e4398174ad957c8f33c4 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 10 May 2021 14:27:29 +0100 Subject: [PATCH 03/69] skip flaky suite (#98974) --- .../apps/visualize/input_control_vis/dynamic_options.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/visualize/input_control_vis/dynamic_options.ts b/test/functional/apps/visualize/input_control_vis/dynamic_options.ts index 25d0e681b40b8..babe5a61a0cbb 100644 --- a/test/functional/apps/visualize/input_control_vis/dynamic_options.ts +++ b/test/functional/apps/visualize/input_control_vis/dynamic_options.ts @@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'visualize', 'visEditor', 'header', 'timePicker']); const comboBox = getService('comboBox'); - describe('dynamic options', () => { + // FLAKY: https://github.com/elastic/kibana/issues/98974 + describe.skip('dynamic options', () => { describe('without chained controls', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('visualize'); From 7695d678d83897f66ffe351ec32934e02a59b066 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 10 May 2021 14:31:32 +0100 Subject: [PATCH 04/69] skip flaky suite (#89031) --- test/functional/apps/management/_test_huge_fields.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.js index b9c9e964ac3f5..dcfb8cf8b4c6b 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.js @@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }) { const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings']); - describe('test large number of fields', function () { + // FLAKY: https://github.com/elastic/kibana/issues/89031 + describe.skip('test large number of fields', function () { this.tags(['skipCloud']); const EXPECTED_FIELD_COUNT = '10006'; From 8b77d7ae129574e8df4601f03a544f6043579bd0 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 10 May 2021 14:40:56 +0100 Subject: [PATCH 05/69] skip flaky suite (#99006) --- x-pack/test/accessibility/apps/advanced_settings.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/advanced_settings.ts b/x-pack/test/accessibility/apps/advanced_settings.ts index 71b8266e72216..7382577c7ebe6 100644 --- a/x-pack/test/accessibility/apps/advanced_settings.ts +++ b/x-pack/test/accessibility/apps/advanced_settings.ts @@ -12,7 +12,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const testSubjects = getService('testSubjects'); - describe('Stack Management -Advanced Settings', () => { + // FLAKY: https://github.com/elastic/kibana/issues/99006 + describe.skip('Stack Management -Advanced Settings', () => { // click on Management > Advanced settings it('click on advanced settings ', async () => { await PageObjects.common.navigateToUrl('management', 'kibana/settings', { From 23bef5193a268659cf202fee82a6a655ec3651ef Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 10 May 2021 10:14:03 -0400 Subject: [PATCH 06/69] [Uptime] remove border from waterfall chart sidebar panel (#99079) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../monitor/synthetics/waterfall/components/sidebar.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx index 0e57a210f032a..6edab485b9606 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx @@ -32,7 +32,11 @@ export const Sidebar: React.FC = ({ items, render }) => { height={items.length * FIXED_AXIS_HEIGHT} data-test-subj="wfSidebarContainer" > - + Date: Mon, 10 May 2021 10:15:01 -0400 Subject: [PATCH 07/69] [Uptime] remove settings flaky test comment (#99402) * remove outdated comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/test/functional/apps/uptime/settings.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/test/functional/apps/uptime/settings.ts b/x-pack/test/functional/apps/uptime/settings.ts index e3cf163d01d80..310a5cc0c9970 100644 --- a/x-pack/test/functional/apps/uptime/settings.ts +++ b/x-pack/test/functional/apps/uptime/settings.ts @@ -50,7 +50,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(await settings.applyButtonIsDisabled()).to.eql(true); }); - // Failing: https://github.com/elastic/kibana/issues/60863 it('changing index pattern setting is reflected elsewhere in UI', async () => { const settings = uptimeService.settings; From 9715157467d088cd0415855d5329d35efbe7c2f1 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 10 May 2021 10:15:49 -0400 Subject: [PATCH 08/69] [Uptime] add data mock to rtl helpers (#99483) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../alerts/monitor_status_alert/alert_monitor_status.test.tsx | 3 +-- x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx index 4120e5987a06e..e161727b46b1b 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx @@ -10,8 +10,7 @@ import { screen } from '@testing-library/dom'; import { AlertMonitorStatusComponent, AlertMonitorStatusProps } from './alert_monitor_status'; import { render } from '../../../../lib/helper/rtl_helpers'; -// Failing: See https://github.com/elastic/kibana/issues/98910 -describe.skip('alert monitor status component', () => { +describe('alert monitor status component', () => { describe('AlertMonitorStatus', () => { const defaultProps: AlertMonitorStatusProps = { alertParams: { diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index b4543a26c875b..a84209a23449a 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -28,6 +28,7 @@ import { AppState } from '../../state'; import { stringifyUrlParams } from './stringify_url_params'; import { ClientPluginsStart } from '../../apps/plugin'; import { triggersActionsUiMock } from '../../../../triggers_actions_ui/public/mocks'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; interface KibanaProps { services?: KibanaServices; @@ -104,6 +105,7 @@ const mockCore: () => Partial = () => { }, triggersActionsUi: triggersActionsUiMock.createStart(), storage: createMockStore(), + data: dataPluginMock.createStartContract(), }; return core; From da890fd24c9ffb5c92ad400730a4bed957aefd48 Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Mon, 10 May 2021 16:18:48 +0200 Subject: [PATCH 09/69] [Security Solution][Endpoint] Validate path values for trusted apps (#99035) * Validate path values for trusted apps show soft warnings when path values are not valid. refs elastic/security-team/issues/315 * use case insensitive flag refs 71ac9bdeafa1dc1746d05ced71f7ca43810fc9d1 * correct check for windows paths review changes * rename review changes * add validations to include ? for wildcards also add more tests refs elastic/security-team/issues/315 * update copy for soft errors refs elastic/security-team/issues/315 * refactor validation logic review changes refs elastic/kibana/pull/99035#discussion_r625106658 * allow wildcards in path names refs elastic/security-team/issues/315 * stack soft errors refs elastic/security-team/issues/315 * Update x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> * remove links to private repos review changes * improve windows path regex refactor tests for better debugging review changes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> --- .../service/trusted_apps/validations.test.ts | 506 ++++++++++++++++++ .../service/trusted_apps/validations.ts | 92 +++- .../components/create_trusted_app_form.tsx | 29 +- 3 files changed, 621 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.test.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.test.ts b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.test.ts new file mode 100644 index 0000000000000..ae95c21630bd8 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.test.ts @@ -0,0 +1,506 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isPathValid } from './validations'; +import { OperatingSystem, ConditionEntryField } from '../../types'; + +describe('Unacceptable Windows wildcard paths', () => { + it('should not accept paths that do not have a folder name with a wildcard ', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'c:\\folder', + }) + ).toEqual(false); + }); + + it('should not accept paths that do not have a file name with a wildcard ', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'c:\\path.exe', + }) + ).toEqual(false); + }); + + it('should not accept nested paths that do not have a wildcard', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'c:\\folder\\path.exe', + }) + ).toEqual(false); + }); + + it('should not accept paths with * wildcard and /', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'c:/**/path.exe', + }) + ).toEqual(false); + }); + + it('should not accept paths with ? wildcard and /', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'C:/?indows/pat?', + }) + ).toEqual(false); + }); +}); + +describe('Acceptable Windows wildcard paths', () => { + it('should accept wildcards for folders', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'c:\\**\\path.exe', + }) + ).toEqual(true); + }); + + it('should accept wildcards for folders and files', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'e:\\**\\*.exe', + }) + ).toEqual(true); + }); + + it('should accept paths with single wildcard', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'f:\\*', + }) + ).toEqual(true); + + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'f:\\?', + }) + ).toEqual(true); + }); + + it('should accept paths that have wildcard in filenames', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'a:\\*.*', + }) + ).toEqual(true); + }); + + it('should accept paths with ? as wildcard', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'C:\\?indows\\pat?', + }) + ).toEqual(true); + }); + + it('should accept paths with both ? and * as wildcards', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'C:\\*?', + }) + ).toEqual(true); + }); + + it('should accept paths with multiple wildcards', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'C:\\**', + }) + ).toEqual(true); + + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'C:\\??', + }) + ).toEqual(true); + }); +}); + +describe('Acceptable Windows exact paths', () => { + it('should accept paths when it ends with a folder name', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'match', + value: 'c:\\folder', + }) + ).toEqual(true); + }); + + it('should accept paths when it ends with a file name', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'match', + value: 'c:\\path.exe', + }) + ).toEqual(true); + }); + + it('should accept paths when it ends with a filename in a folder', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'match', + value: 'c:\\folder\\path.exe', + }) + ).toEqual(true); + }); +}); + +describe('Acceptable Windows exact paths with hyphens', () => { + it('should accept paths when paths have folder names with hyphens', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'match', + value: 'c:\\hype-folder-name', + }) + ).toEqual(true); + }); + + it('should accept paths when file names have hyphens', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'match', + value: 'c:\\file-name.exe', + }) + ).toEqual(true); + }); +}); + +describe('Unacceptable Windows exact paths', () => { + it('should not accept paths with /', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'match', + value: 'c:/folder/path.exe', + }) + ).toEqual(false); + }); + + it('should not accept paths not having a in the suffix', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'match', + value: '\\folder\\path.exe', + }) + ).toEqual(false); + }); +}); + +/// +describe('Unacceptable Mac/Linux wildcard paths', () => { + it('should not accept paths that do not have a folder name with a wildcard ', () => { + expect( + isPathValid({ + os: OperatingSystem.MAC, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: '/folder', + }) + ).toEqual(false); + }); + + it('should not accept paths that do not have a file name with a wildcard ', () => { + expect( + isPathValid({ + os: OperatingSystem.LINUX, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: '/zip.zip', + }) + ).toEqual(false); + }); + + it('should not accept nested paths that do not have a wildcard', () => { + expect( + isPathValid({ + os: OperatingSystem.MAC, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: '/opt/pack.tar', + }) + ).toEqual(false); + }); + + it('should not accept paths with * wildcard and \\', () => { + expect( + isPathValid({ + os: OperatingSystem.LINUX, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'c:\\**\\path.exe', + }) + ).toEqual(false); + }); + + it('should not accept paths with ? wildcard and \\', () => { + expect( + isPathValid({ + os: OperatingSystem.LINUX, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: 'C:\\?indows\\pat?', + }) + ).toEqual(false); + }); +}); + +describe('Acceptable Mac/Linux wildcard paths', () => { + it('should accept wildcards for folders', () => { + expect( + isPathValid({ + os: OperatingSystem.MAC, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: '/**/file.', + }) + ).toEqual(true); + }); + + it('should accept wildcards for folders and files', () => { + expect( + isPathValid({ + os: OperatingSystem.LINUX, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: '/usr/bi?/*.js', + }) + ).toEqual(true); + }); + + it('should accept paths with single wildcard', () => { + expect( + isPathValid({ + os: OperatingSystem.MAC, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: '/op*', + }) + ).toEqual(true); + + expect( + isPathValid({ + os: OperatingSystem.LINUX, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: '/op?', + }) + ).toEqual(true); + }); + + it('should accept paths that have wildcard in filenames', () => { + expect( + isPathValid({ + os: OperatingSystem.MAC, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: '/*.*', + }) + ).toEqual(true); + }); + + it('should accept paths with ? as wildcard', () => { + expect( + isPathValid({ + os: OperatingSystem.LINUX, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: '/usr/?inux/pat?', + }) + ).toEqual(true); + }); + + it('should accept paths with both ? and * as wildcards', () => { + expect( + isPathValid({ + os: OperatingSystem.MAC, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: '/usr/*?', + }) + ).toEqual(true); + }); + + it('should accept paths with multiple wildcards', () => { + expect( + isPathValid({ + os: OperatingSystem.LINUX, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: '/usr/**', + }) + ).toEqual(true); + + expect( + isPathValid({ + os: OperatingSystem.MAC, + field: ConditionEntryField.PATH, + type: 'wildcard', + value: '/opt/??', + }) + ).toEqual(true); + }); +}); + +describe('Acceptable Mac/Linux exact paths', () => { + it('should accept paths when it is the root path', () => { + expect( + isPathValid({ + os: OperatingSystem.LINUX, + field: ConditionEntryField.PATH, + type: 'match', + value: '/', + }) + ).toEqual(true); + }); + + it('should accept paths when it ends with a file name', () => { + expect( + isPathValid({ + os: OperatingSystem.MAC, + field: ConditionEntryField.PATH, + type: 'match', + value: '/usr/file.ts', + }) + ).toEqual(true); + }); + + it('should accept paths when it ends with a filename in a folder', () => { + expect( + isPathValid({ + os: OperatingSystem.LINUX, + field: ConditionEntryField.PATH, + type: 'match', + value: '/opt/z.dmg', + }) + ).toEqual(true); + }); +}); + +describe('Acceptable Mac/Linux exact paths with hyphens', () => { + it('should accept paths when paths have folder names with hyphens', () => { + expect( + isPathValid({ + os: OperatingSystem.MAC, + field: ConditionEntryField.PATH, + type: 'match', + value: '/hype-folder-name', + }) + ).toEqual(true); + }); + + it('should accept paths when file names have hyphens', () => { + expect( + isPathValid({ + os: OperatingSystem.LINUX, + field: ConditionEntryField.PATH, + type: 'match', + value: '/file-name.dmg', + }) + ).toEqual(true); + }); +}); + +describe('Unacceptable Mac/Linux exact paths', () => { + it('should not accept paths with \\', () => { + expect( + isPathValid({ + os: OperatingSystem.MAC, + field: ConditionEntryField.PATH, + type: 'match', + value: 'c:\\folder\\path.exe', + }) + ).toEqual(false); + }); + + it('should not accept paths not starting with /', () => { + expect( + isPathValid({ + os: OperatingSystem.LINUX, + field: ConditionEntryField.PATH, + type: 'match', + value: 'opt/bin', + }) + ).toEqual(false); + }); + + it('should not accept paths ending with /', () => { + expect( + isPathValid({ + os: OperatingSystem.MAC, + field: ConditionEntryField.PATH, + type: 'match', + value: '/opt/bin/', + }) + ).toEqual(false); + }); + + it('should not accept file extensions with hyphens', () => { + expect( + isPathValid({ + os: OperatingSystem.LINUX, + field: ConditionEntryField.PATH, + type: 'match', + value: '/opt/bin/file.d-mg', + }) + ).toEqual(false); + }); +}); diff --git a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts index b0828be6af6c5..37aeec8adea80 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { ConditionEntry, ConditionEntryField } from '../../types'; +import { + ConditionEntry, + ConditionEntryField, + OperatingSystem, + TrustedAppEntryTypes, +} from '../../types'; const HASH_LENGTHS: readonly number[] = [ 32, // MD5 @@ -28,3 +33,88 @@ export const getDuplicateFields = (entries: ConditionEntry[]) => { .filter((entry) => entry[1].length > 1) .map((entry) => entry[0]); }; + +export const isPathValid = ({ + os, + field, + type, + value, +}: { + os: OperatingSystem; + field: ConditionEntryField; + type: TrustedAppEntryTypes; + value: string; +}): boolean => { + if (field === ConditionEntryField.PATH) { + if (type === 'wildcard') { + return os === OperatingSystem.WINDOWS + ? isWindowsWildcardPathValid(value) + : isLinuxMacWildcardPathValid(value); + } + return doesPathMatchRegex({ value, os }); + } + return true; +}; + +const doesPathMatchRegex = ({ os, value }: { os: OperatingSystem; value: string }): boolean => { + if (os === OperatingSystem.WINDOWS) { + const filePathRegex = /^[a-z]:(?:|\\\\[^<>:"'/\\|?*]+\\[^<>:"'/\\|?*]+|%\w+%|)[\\](?:[^<>:"'/\\|?*]+[\\/])*([^<>:"'/\\|?*])+$/i; + return filePathRegex.test(value); + } + return /^(\/|(\/[\w\-]+)+|\/[\w\-]+\.[\w]+|(\/[\w-]+)+\/[\w\-]+\.[\w]+)$/i.test(value); +}; + +const isWindowsWildcardPathValid = (path: string): boolean => { + const firstCharacter = path[0]; + const lastCharacter = path.slice(-1); + const trimmedValue = path.trim(); + const hasSlash = /\//.test(trimmedValue); + if (path.length === 0) { + return false; + } else if ( + hasSlash || + trimmedValue.length !== path.length || + firstCharacter === '^' || + lastCharacter === '\\' || + !hasWildcard({ path, isWindowsPath: true }) + ) { + return false; + } else { + return true; + } +}; + +const isLinuxMacWildcardPathValid = (path: string): boolean => { + const firstCharacter = path[0]; + const lastCharacter = path.slice(-1); + const trimmedValue = path.trim(); + if (path.length === 0) { + return false; + } else if ( + trimmedValue.length !== path.length || + firstCharacter !== '/' || + lastCharacter === '/' || + path.length > 1024 === true || + path.includes('//') === true || + !hasWildcard({ path, isWindowsPath: false }) + ) { + return false; + } else { + return true; + } +}; + +const hasWildcard = ({ + path, + isWindowsPath, +}: { + path: string; + isWindowsPath: boolean; +}): boolean => { + for (const pathComponent of path.split(isWindowsPath ? '\\' : '/')) { + if (/[\*|\?]+/.test(pathComponent) === true) { + return true; + } + } + return false; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx index fe1b40aac2322..562427e27592b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx @@ -25,7 +25,10 @@ import { NewTrustedApp, OperatingSystem, } from '../../../../../../common/endpoint/types'; -import { isValidHash } from '../../../../../../common/endpoint/service/trusted_apps/validations'; +import { + isValidHash, + isPathValid, +} from '../../../../../../common/endpoint/service/trusted_apps/validations'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { @@ -53,8 +56,8 @@ const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ interface FieldValidationState { /** If this fields state is invalid. Drives display of errors on the UI */ isInvalid: boolean; - errors: string[]; - warnings: string[]; + errors: React.ReactNode[]; + warnings: React.ReactNode[]; } interface ValidationResult { /** Overall indicator if form is valid */ @@ -72,7 +75,7 @@ const addResultToValidation = ( validation: ValidationResult, field: keyof NewTrustedApp, type: 'warnings' | 'errors', - resultValue: string + resultValue: React.ReactNode ) => { if (!validation.result[field]) { validation.result[field] = { @@ -81,7 +84,8 @@ const addResultToValidation = ( warnings: [], }; } - validation.result[field]![type].push(resultValue); + const errorMarkup: React.ReactNode = type === 'warnings' ?
{resultValue}
: resultValue; + validation.result[field]![type].push(errorMarkup); validation.result[field]!.isInvalid = true; }; @@ -154,6 +158,20 @@ const validateFormValues = (values: MaybeImmutable): ValidationRe values: { row: index + 1 }, }) ); + } else if ( + !isPathValid({ os: values.os, field: entry.field, type: entry.type, value: entry.value }) + ) { + // show soft warnings and thus allow entry + isValid = true; + addResultToValidation( + validation, + 'entries', + 'warnings', + i18n.translate('xpack.securitySolution.trustedapps.create.conditionFieldInvalidPathMsg', { + defaultMessage: '[{row}] Path may be formed incorrectly; verify value', + values: { row: index + 1 }, + }) + ); } }); } @@ -468,6 +486,7 @@ export const CreateTrustedAppForm = memo( data-test-subj={getTestId('conditionsRow')} isInvalid={wasVisited?.entries && validationResult.result.entries?.isInvalid} error={validationResult.result.entries?.errors} + helpText={validationResult.result.entries?.warnings} > Date: Mon, 10 May 2021 10:19:38 -0400 Subject: [PATCH 10/69] [Rollups] remove use of custom cluster client (#99623) --- x-pack/plugins/rollup/server/plugin.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts index ff6adc1c8d24b..5c15ec0263dc3 100644 --- a/x-pack/plugins/rollup/server/plugin.ts +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -9,7 +9,6 @@ import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { CoreSetup, - ILegacyCustomClusterClient, Plugin, Logger, PluginInitializerContext, @@ -33,7 +32,6 @@ export class RollupPlugin implements Plugin { private readonly logger: Logger; private readonly globalConfig$: Observable; private readonly license: License; - private rollupEsClient?: ILegacyCustomClusterClient; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); @@ -121,9 +119,5 @@ export class RollupPlugin implements Plugin { start() {} - stop() { - if (this.rollupEsClient) { - this.rollupEsClient.close(); - } - } + stop() {} } From 0ffe4c7a546bfad63305a85ce62b81146bf4cca8 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Mon, 10 May 2021 16:58:50 +0200 Subject: [PATCH 11/69] [SecuritySolution] Add success toast to timeline deletion (#99612) * Add success toast to timeline deletion * Add unit tests for timeline deletion toast * Refactor export_timeline to use useAppToasts instead of useStateToaster --- .../delete_timeline_modal/index.test.tsx | 35 ++++++++++++++++- .../delete_timeline_modal/index.tsx | 19 ++++++++- .../export_timeline/export_timeline.test.tsx | 39 +++++++------------ .../export_timeline/export_timeline.tsx | 24 +++++------- .../components/open_timeline/translations.ts | 14 +++++++ 5 files changed, 87 insertions(+), 44 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx index cfbc7d255062f..54b405feeb176 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx @@ -11,6 +11,10 @@ import { useParams } from 'react-router-dom'; import { DeleteTimelineModalOverlay } from '.'; import { TimelineType } from '../../../../../common/types/timeline'; +import * as i18n from '../translations'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; + +jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('react-router-dom', () => { const actual = jest.requireActual('react-router-dom'); @@ -21,12 +25,19 @@ jest.mock('react-router-dom', () => { }); describe('DeleteTimelineModal', () => { - const savedObjectId = 'abcd'; + const mockAddSuccess = jest.fn(); + (useAppToasts as jest.Mock).mockReturnValue({ addSuccess: mockAddSuccess }); + + afterEach(() => { + mockAddSuccess.mockClear(); + }); + + const savedObjectIds = ['abcd']; const defaultProps = { closeModal: jest.fn(), deleteTimelines: jest.fn(), isModalOpen: true, - savedObjectIds: [savedObjectId], + savedObjectIds, title: 'Privilege Escalation', }; @@ -56,5 +67,25 @@ describe('DeleteTimelineModal', () => { expect(wrapper.find('[data-test-subj="remove-popover"]').first().exists()).toBe(true); }); + + test('it shows correct toast message on success for deleted timelines', async () => { + const wrapper = mountWithIntl(); + wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click'); + + expect(mockAddSuccess.mock.calls[0][0].title).toEqual( + i18n.SUCCESSFULLY_DELETED_TIMELINES(savedObjectIds.length) + ); + }); + + test('it shows correct toast message on success for deleted templates', async () => { + (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.template }); + + const wrapper = mountWithIntl(); + wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click'); + + expect(mockAddSuccess.mock.calls[0][0].title).toEqual( + i18n.SUCCESSFULLY_DELETED_TIMELINE_TEMPLATES(savedObjectIds.length) + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx index 7dde3fbe4cd2a..41e491ccc0ceb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx @@ -9,8 +9,13 @@ import { EuiModal } from '@elastic/eui'; import React, { useCallback } from 'react'; import { createGlobalStyle } from 'styled-components'; +import { useParams } from 'react-router-dom'; import { DeleteTimelineModal, DELETE_TIMELINE_MODAL_WIDTH } from './delete_timeline_modal'; import { DeleteTimelines } from '../types'; +import { TimelineType } from '../../../../../common/types/timeline'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import * as i18n from '../translations'; + const RemovePopover = createGlobalStyle` div.euiPopover__panel-isOpen { display: none; @@ -29,19 +34,29 @@ interface Props { */ export const DeleteTimelineModalOverlay = React.memo( ({ deleteTimelines, isModalOpen, savedObjectIds, title, onComplete }) => { + const { addSuccess } = useAppToasts(); + const { tabName: timelineType } = useParams<{ tabName: TimelineType }>(); + const internalCloseModal = useCallback(() => { if (onComplete != null) { onComplete(); } }, [onComplete]); const onDelete = useCallback(() => { - if (savedObjectIds != null) { + if (savedObjectIds.length > 0) { deleteTimelines(savedObjectIds); + + addSuccess({ + title: + timelineType === TimelineType.template + ? i18n.SUCCESSFULLY_DELETED_TIMELINE_TEMPLATES(savedObjectIds.length) + : i18n.SUCCESSFULLY_DELETED_TIMELINES(savedObjectIds.length), + }); } if (onComplete != null) { onComplete(); } - }, [deleteTimelines, savedObjectIds, onComplete]); + }, [deleteTimelines, savedObjectIds, onComplete, addSuccess, timelineType]); return ( <> {isModalOpen && } diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx index feb30364fba23..a273ef1df9788 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; -import { useStateToaster } from '../../../../common/components/toasters'; import { TimelineDownloader } from './export_timeline'; import { mockSelectedTimeline } from './mocks'; @@ -16,12 +15,9 @@ import { ReactWrapper, mount } from 'enzyme'; import { waitFor } from '@testing-library/react'; import { useParams } from 'react-router-dom'; -jest.mock('../translations', () => { - return { - EXPORT_SELECTED: 'EXPORT_SELECTED', - EXPORT_FILENAME: 'TIMELINE', - }; -}); +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; + +jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('.', () => { return { @@ -38,34 +34,26 @@ jest.mock('react-router-dom', () => { }; }); -jest.mock('../../../../common/components/toasters', () => { - const actual = jest.requireActual('../../../../common/components/toasters'); - return { - ...actual, - useStateToaster: jest.fn(), - }; -}); - describe('TimelineDownloader', () => { + const mockAddSuccess = jest.fn(); + (useAppToasts as jest.Mock).mockReturnValue({ addSuccess: mockAddSuccess }); + let wrapper: ReactWrapper; + const exportedIds = ['baa20980-6301-11ea-9223-95b6d4dd806c']; const defaultTestProps = { - exportedIds: ['baa20980-6301-11ea-9223-95b6d4dd806c'], + exportedIds, getExportedData: jest.fn(), isEnableDownloader: true, onComplete: jest.fn(), }; - const mockDispatchToaster = jest.fn(); beforeEach(() => { - (useStateToaster as jest.Mock).mockReturnValue([jest.fn(), mockDispatchToaster]); (useParams as jest.Mock).mockReturnValue({ tabName: 'default' }); }); afterEach(() => { - (useStateToaster as jest.Mock).mockClear(); (useParams as jest.Mock).mockReset(); - - (mockDispatchToaster as jest.Mock).mockClear(); + mockAddSuccess.mockClear(); }); describe('should not render a downloader', () => { @@ -104,11 +92,12 @@ describe('TimelineDownloader', () => { }; wrapper = mount(); + await waitFor(() => { wrapper.update(); - expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( - i18n.SUCCESSFULLY_EXPORTED_TIMELINES + expect(mockAddSuccess.mock.calls[0][0].title).toEqual( + i18n.SUCCESSFULLY_EXPORTED_TIMELINES(exportedIds.length) ); }); }); @@ -124,8 +113,8 @@ describe('TimelineDownloader', () => { await waitFor(() => { wrapper.update(); - expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( - i18n.SUCCESSFULLY_EXPORTED_TIMELINES + expect(mockAddSuccess.mock.calls[0][0].title).toEqual( + i18n.SUCCESSFULLY_EXPORTED_TIMELINE_TEMPLATES(exportedIds.length) ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.tsx index 01f18b5ad9c3d..b8b1c76ffd6d7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.tsx @@ -6,7 +6,6 @@ */ import React, { useCallback } from 'react'; -import uuid from 'uuid'; import { useParams } from 'react-router-dom'; import { @@ -14,8 +13,8 @@ import { ExportSelectedData, } from '../../../../common/components/generic_downloader'; import * as i18n from '../translations'; -import { useStateToaster } from '../../../../common/components/toasters'; import { TimelineType } from '../../../../../common/types/timeline'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; const ExportTimeline: React.FC<{ exportedIds: string[] | undefined; @@ -23,8 +22,8 @@ const ExportTimeline: React.FC<{ isEnableDownloader: boolean; onComplete?: () => void; }> = ({ onComplete, isEnableDownloader, exportedIds, getExportedData }) => { - const [, dispatchToaster] = useStateToaster(); const { tabName: timelineType } = useParams<{ tabName: TimelineType }>(); + const { addSuccess } = useAppToasts(); const onExportSuccess = useCallback( (exportCount) => { @@ -32,20 +31,15 @@ const ExportTimeline: React.FC<{ onComplete(); } - dispatchToaster({ - type: 'addToaster', - toast: { - id: uuid.v4(), - title: - timelineType === TimelineType.template - ? i18n.SUCCESSFULLY_EXPORTED_TIMELINE_TEMPLATES(exportCount) - : i18n.SUCCESSFULLY_EXPORTED_TIMELINES(exportCount), - color: 'success', - iconType: 'check', - }, + addSuccess({ + title: + timelineType === TimelineType.template + ? i18n.SUCCESSFULLY_EXPORTED_TIMELINE_TEMPLATES(exportCount) + : i18n.SUCCESSFULLY_EXPORTED_TIMELINES(exportCount), + 'data-test-subj': 'addObjectToContainerSuccess', }); }, - [dispatchToaster, onComplete, timelineType] + [addSuccess, onComplete, timelineType] ); const onExportFailure = useCallback(() => { if (onComplete != null) { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts index 4858bf3ed6083..40af4514e26a3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts @@ -293,6 +293,20 @@ export const SUCCESSFULLY_EXPORTED_TIMELINE_TEMPLATES = (totalTimelineTemplates: } ); +export const SUCCESSFULLY_DELETED_TIMELINES = (totalTimelines: number) => + i18n.translate('xpack.securitySolution.open.timeline.successfullyDeletedTimelinesTitle', { + values: { totalTimelines }, + defaultMessage: + 'Successfully deleted {totalTimelines, plural, =0 {all timelines} =1 {{totalTimelines} timeline} other {{totalTimelines} timelines}}', + }); + +export const SUCCESSFULLY_DELETED_TIMELINE_TEMPLATES = (totalTimelineTemplates: number) => + i18n.translate('xpack.securitySolution.open.timeline.successfullyDeletedTimelineTemplatesTitle', { + values: { totalTimelineTemplates }, + defaultMessage: + 'Successfully deleted {totalTimelineTemplates, plural, =0 {all timelines} =1 {{totalTimelineTemplates} timeline template} other {{totalTimelineTemplates} timeline templates}}', + }); + export const TAB_TIMELINES = i18n.translate( 'xpack.securitySolution.timelines.components.tabs.timelinesTitle', { From 518da5bcc1db516c473abf04fb1c1d87485211e7 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 10 May 2021 17:54:55 +0200 Subject: [PATCH 12/69] [SecuritySolution] Histogram IP legends error fixed (#99468) * make sure stackByField exists * fix types * fix unit test * skip extra request for non-ip queries * elasticserach query changes to prevent corrupted data response bug * client changes to split ip stacked histogram queries in two, inspect modal shows all requests and responses * lint fixes * test for useMatrixHistogramCombined added * comment added on new multiple prop * changed query to always contain value_type:ip for ip queries Co-authored-by: Angela Chuang Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../matrix_histogram/index.ts | 1 + .../components/header_section/index.tsx | 4 +- .../common/components/inspect/index.tsx | 30 ++- .../common/components/inspect/modal.tsx | 102 ++++++---- .../matrix_histogram/index.test.tsx | 9 +- .../components/matrix_histogram/index.tsx | 14 +- .../components/matrix_histogram/types.ts | 2 + .../components/matrix_histogram/utils.test.ts | 4 +- .../components/matrix_histogram/utils.ts | 12 +- .../public/common/components/top_n/top_n.tsx | 1 + .../containers/matrix_histogram/index.test.ts | 64 +++++- .../containers/matrix_histogram/index.ts | 75 +++++++- .../alerts_by_category/index.test.tsx | 8 +- .../components/events_by_dataset/index.tsx | 3 + .../events/__mocks__/index.ts | 182 ++++++++++++++++++ .../events/query.events_histogram.dsl.test.ts | 85 +++----- .../events/query.events_histogram.dsl.ts | 45 ++++- 17 files changed, 510 insertions(+), 131 deletions(-) diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts index fd1cf32e21400..0d624980f57d7 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts @@ -50,6 +50,7 @@ export interface MatrixHistogramRequestOptions extends RequestBasicOptions { | undefined; inspect?: Maybe; isPtrIncluded?: boolean; + includeMissingData?: boolean; } export interface MatrixHistogramStrategyResponse extends IEsSearchResponse { diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx index 8578ccb796b28..fb8022292d329 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx @@ -47,6 +47,7 @@ export interface HeaderSectionProps extends HeaderProps { titleSize?: EuiTitleSize; tooltip?: string; growLeftSplit?: boolean; + inspectMultiple?: boolean; } const HeaderSectionComponent: React.FC = ({ @@ -60,6 +61,7 @@ const HeaderSectionComponent: React.FC = ({ titleSize = 'm', tooltip, growLeftSplit = true, + inspectMultiple = false, }) => (
@@ -83,7 +85,7 @@ const HeaderSectionComponent: React.FC = ({ {id && ( - + )} diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx index 458c7d02b1dce..ddbcf710aff30 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx @@ -59,6 +59,7 @@ interface OwnProps { isDisabled?: boolean; onCloseInspect?: () => void; title: string | React.ReactElement | React.ReactNode; + multiple?: boolean; } type InspectButtonProps = OwnProps & PropsFromRedux; @@ -71,6 +72,7 @@ const InspectButtonComponent: React.FC = ({ isInspected, loading, inspectIndex = 0, + multiple = false, // If multiple = true we ignore the inspectIndex and pass all requests and responses to the inspect modal onCloseInspect, queryId = '', selectedInspectIndex, @@ -99,6 +101,26 @@ const InspectButtonComponent: React.FC = ({ }); }, [onCloseInspect, setIsInspected, queryId, inputId, inspectIndex]); + let request: string | null = null; + let additionalRequests: string[] | null = null; + if (inspect != null && inspect.dsl.length > 0) { + if (multiple) { + [request, ...additionalRequests] = inspect.dsl; + } else { + request = inspect.dsl[inspectIndex]; + } + } + + let response: string | null = null; + let additionalResponses: string[] | null = null; + if (inspect != null && inspect.response.length > 0) { + if (multiple) { + [response, ...additionalResponses] = inspect.response; + } else { + response = inspect.response[inspectIndex]; + } + } + return ( <> {inputId === 'timeline' && !compact && ( @@ -131,10 +153,10 @@ const InspectButtonComponent: React.FC = ({ 0 ? inspect.dsl[inspectIndex] : null} - response={ - inspect != null && inspect.response.length > 0 ? inspect.response[inspectIndex] : null - } + request={request} + response={response} + additionalRequests={additionalRequests} + additionalResponses={additionalResponses} title={title} data-test-subj="inspect-modal" /> diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx index a5c0144531110..ce432940247e9 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx @@ -19,7 +19,7 @@ import { EuiTabbedContent, } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import React, { ReactNode } from 'react'; +import React, { Fragment, ReactNode } from 'react'; import styled from 'styled-components'; import { NO_ALERT_INDEX } from '../../../../common/constants'; @@ -44,6 +44,8 @@ interface ModalInspectProps { isShowing: boolean; request: string | null; response: string | null; + additionalRequests?: string[] | null; + additionalResponses?: string[] | null; title: string | React.ReactElement | React.ReactNode; } @@ -73,11 +75,11 @@ const MyEuiModal = styled(EuiModal)` `; MyEuiModal.displayName = 'MyEuiModal'; -const parseInspectString = function (objectStringify: string): T | null { +const parseInspectStrings = function (stringsArray: string[]): T[] { try { - return JSON.parse(objectStringify); + return stringsArray.map((objectStringify) => JSON.parse(objectStringify)); } catch { - return null; + return []; } }; @@ -103,13 +105,23 @@ export const ModalInspectQuery = ({ isShowing = false, request, response, + additionalRequests, + additionalResponses, title, }: ModalInspectProps) => { if (!isShowing || request == null || response == null) { return null; } - const inspectRequest: Request | null = parseInspectString(request); - const inspectResponse: Response | null = parseInspectString(response); + + const requests: string[] = [request, ...(additionalRequests != null ? additionalRequests : [])]; + const responses: string[] = [ + response, + ...(additionalResponses != null ? additionalResponses : []), + ]; + + const inspectRequests: Request[] = parseInspectStrings(requests); + const inspectResponses: Response[] = parseInspectStrings(responses); + const statistics: Array<{ title: NonNullable; description: NonNullable; @@ -123,7 +135,7 @@ export const ModalInspectQuery = ({ ), description: ( - {formatIndexPatternRequested(inspectRequest?.index ?? [])} + {formatIndexPatternRequested(inspectRequests[0]?.index ?? [])} ), }, @@ -137,8 +149,8 @@ export const ModalInspectQuery = ({ ), description: ( - {inspectResponse != null - ? `${numeral(inspectResponse.took).format('0,0')}ms` + {inspectResponses[0]?.took + ? `${numeral(inspectResponses[0].took).format('0,0')}ms` : i18n.SOMETHING_WENT_WRONG} ), @@ -170,42 +182,50 @@ export const ModalInspectQuery = ({ { id: 'request', name: 'Request', - content: ( - <> - - - {inspectRequest != null - ? manageStringify(inspectRequest.body) - : i18n.SOMETHING_WENT_WRONG} - - - ), + content: + inspectRequests.length > 0 ? ( + inspectRequests.map((inspectRequest, index) => ( + + + + {manageStringify(inspectRequest.body)} + + + )) + ) : ( + {i18n.SOMETHING_WENT_WRONG} + ), }, { id: 'response', name: 'Response', - content: ( - <> - - - {response} - - - ), + content: + inspectResponses.length > 0 ? ( + responses.map((responseText, index) => ( + + + + {responseText} + + + )) + ) : ( + {i18n.SOMETHING_WENT_WRONG} + ), }, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx index 49698ef527718..7c23faa433494 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx @@ -11,7 +11,7 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; import { MatrixHistogram } from '.'; -import { useMatrixHistogram } from '../../containers/matrix_histogram'; +import { useMatrixHistogramCombined } from '../../containers/matrix_histogram'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { TestProviders } from '../../mock'; @@ -30,7 +30,7 @@ jest.mock('../charts/barchart', () => ({ })); jest.mock('../../containers/matrix_histogram', () => ({ - useMatrixHistogram: jest.fn(), + useMatrixHistogramCombined: jest.fn(), })); jest.mock('../../components/matrix_histogram/utils', () => ({ @@ -63,7 +63,7 @@ describe('Matrix Histogram Component', () => { }; beforeAll(() => { - (useMatrixHistogram as jest.Mock).mockReturnValue([ + (useMatrixHistogramCombined as jest.Mock).mockReturnValue([ false, { data: null, @@ -75,6 +75,7 @@ describe('Matrix Histogram Component', () => { wrappingComponent: TestProviders, }); }); + describe('on initial load', () => { test('it renders MatrixLoader', () => { expect(wrapper.find('MatrixLoader').exists()).toBe(true); @@ -99,7 +100,7 @@ describe('Matrix Histogram Component', () => { describe('not initial load', () => { beforeAll(() => { - (useMatrixHistogram as jest.Mock).mockReturnValue([ + (useMatrixHistogramCombined as jest.Mock).mockReturnValue([ false, { data: [ diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index 693bcbd0d449b..0a80ff0045cd1 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -17,7 +17,7 @@ import { HeaderSection } from '../header_section'; import { MatrixLoader } from './matrix_loader'; import { Panel } from '../panel'; import { getBarchartConfigs, getCustomChartData } from './utils'; -import { useMatrixHistogram } from '../../containers/matrix_histogram'; +import { useMatrixHistogramCombined } from '../../containers/matrix_histogram'; import { MatrixHistogramProps, MatrixHistogramOption, MatrixHistogramQueryProps } from './types'; import { InspectButtonContainer } from '../inspect'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; @@ -40,6 +40,7 @@ export type MatrixHistogramComponentProps = MatrixHistogramProps & id: string; legendPosition?: Position; mapping?: MatrixHistogramMappingTypes; + onError?: () => void; showSpacer?: boolean; setQuery: GlobalTimeArgs['setQuery']; setAbsoluteRangeDatePickerTarget?: InputsModelId; @@ -77,6 +78,7 @@ export const MatrixHistogramComponent: React.FC = isPtrIncluded, legendPosition, mapping, + onError, panelHeight = DEFAULT_PANEL_HEIGHT, setAbsoluteRangeDatePickerTarget = 'global', setQuery, @@ -133,17 +135,22 @@ export const MatrixHistogramComponent: React.FC = [defaultStackByOption, stackByOptions] ); - const [loading, { data, inspect, totalCount, refetch }] = useMatrixHistogram({ + const matrixHistogramRequest = { endDate, errorMessage, filterQuery, histogramType, indexNames, + onError, startDate, stackByField: selectedStackByOption.value, isPtrIncluded, docValueFields, - }); + }; + + const [loading, { data, inspect, totalCount, refetch }] = useMatrixHistogramCombined( + matrixHistogramRequest + ); const titleWithStackByField = useMemo( () => (title != null && typeof title === 'function' ? title(selectedStackByOption) : title), @@ -208,6 +215,7 @@ export const MatrixHistogramComponent: React.FC = title={titleWithStackByField} titleSize={titleSize} subtitle={subtitleWithCounts} + inspectMultiple > diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index 2e0f1ac762ca8..7cb4144e1acba 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -66,6 +66,7 @@ export interface MatrixHistogramQueryProps { errorMessage: string; indexNames: string[]; filterQuery?: ESQuery | string | undefined; + onError?: () => void; setAbsoluteRangeDatePicker?: ActionCreator<{ id: InputsModelId; from: string; @@ -78,6 +79,7 @@ export interface MatrixHistogramQueryProps { threshold?: Threshold; skip?: boolean; isPtrIncluded?: boolean; + includeMissingData?: boolean; } export interface MatrixHistogramProps extends MatrixHistogramBasicProps { diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.test.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.test.ts index d382a514bb51d..633f6ea3ebeb6 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.test.ts @@ -106,7 +106,7 @@ describe('utils', () => { expect(result).toEqual([]); }); - test('shoule format data correctly', () => { + test('should format data correctly', () => { const data = [ { x: 1, y: 2, g: 'g1' }, { x: 2, y: 4, g: 'g1' }, @@ -120,6 +120,7 @@ describe('utils', () => { expect(result).toEqual([ { key: 'g1', + color: '#1EA593', value: [ { x: 1, y: 2, g: 'g1' }, { x: 2, y: 4, g: 'g1' }, @@ -128,6 +129,7 @@ describe('utils', () => { }, { key: 'g2', + color: '#2B70F7', value: [ { x: 1, y: 1, g: 'g2' }, { x: 2, y: 3, g: 'g2' }, diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts index d45605e4aa38f..6594dc30d5ce8 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts @@ -82,6 +82,7 @@ export const defaultLegendColors = [ '#B0916F', '#7B000B', '#34130C', + '#GGGGGG', ]; export const formatToChartDataItem = ([key, value]: [ @@ -100,11 +101,8 @@ export const getCustomChartData = ( const dataGroupedByEvent = groupBy('g', data); const dataGroupedEntries = toPairs(dataGroupedByEvent); const formattedChartData = map(formatToChartDataItem, dataGroupedEntries); - - if (mapping) - return map((item: ChartSeriesData) => { - const mapItem = get(item.key, mapping); - return { ...item, color: mapItem?.color }; - }, formattedChartData); - else return formattedChartData; + return formattedChartData.map((item: ChartSeriesData, idx: number) => { + const mapItem = get(item.key, mapping); + return { ...item, color: mapItem?.color ?? defaultLegendColors[idx] }; + }); }; diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx index cc9be327cc498..9d38d6b4a59e3 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx @@ -129,6 +129,7 @@ const TopNComponent: React.FC = ({ setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} setQuery={setQuery} showSpacer={false} + toggleTopN={toggleTopN} timelineId={timelineId} to={to} /> diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.ts index 045ffc6d26b4b..c65c1a8fdd841 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.ts @@ -8,7 +8,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useKibana } from '../../../common/lib/kibana'; -import { useMatrixHistogram } from '.'; +import { useMatrixHistogram, useMatrixHistogramCombined } from '.'; import { MatrixHistogramType } from '../../../../common/search_strategy'; import { TestProviders } from '../../mock/test_providers'; @@ -25,6 +25,10 @@ describe('useMatrixHistogram', () => { startDate: new Date(Date.now()).toISOString(), }; + afterEach(() => { + (useKibana().services.data.search.search as jest.Mock).mockClear(); + }); + it('should update request when props has changed', async () => { const localProps = { ...props }; const { rerender } = renderHook(() => useMatrixHistogram(localProps), { @@ -54,3 +58,61 @@ describe('useMatrixHistogram', () => { expect(result1).toBe(result2); }); }); + +describe('useMatrixHistogramCombined', () => { + const props = { + endDate: new Date(Date.now()).toISOString(), + errorMessage: '', + filterQuery: {}, + histogramType: MatrixHistogramType.events, + indexNames: [], + stackByField: 'event.module', + startDate: new Date(Date.now()).toISOString(), + }; + + afterEach(() => { + (useKibana().services.data.search.search as jest.Mock).mockClear(); + }); + + it('should update request when props has changed', async () => { + const localProps = { ...props }; + const { rerender } = renderHook(() => useMatrixHistogramCombined(localProps), { + wrapper: TestProviders, + }); + + localProps.stackByField = 'event.action'; + + rerender(); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toBe(2); + expect(mockCalls[0][0].stackByField).toBe('event.module'); + expect(mockCalls[1][0].stackByField).toBe('event.action'); + }); + + it('should do two request when stacking by ip field', async () => { + const localProps = { ...props, stackByField: 'source.ip' }; + renderHook(() => useMatrixHistogramCombined(localProps), { + wrapper: TestProviders, + }); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toBe(2); + expect(mockCalls[0][0].stackByField).toBe('source.ip'); + expect(mockCalls[1][0].stackByField).toBe('source.ip'); + }); + + it('returns a memoized value', async () => { + const { result, rerender } = renderHook(() => useMatrixHistogramCombined(props), { + wrapper: TestProviders, + }); + + const result1 = result.current[1]; + act(() => rerender()); + const result2 = result.current[1]; + + expect(result1).toBe(result2); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index edc2d1e233c6e..d71a1a843269f 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -7,7 +7,7 @@ import deepEqual from 'fast-deep-equal'; import { getOr, isEmpty, noop } from 'lodash/fp'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Subscription } from 'rxjs'; import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; @@ -53,10 +53,12 @@ export const useMatrixHistogram = ({ histogramType, indexNames, isPtrIncluded, + onError, stackByField, startDate, threshold, skip = false, + includeMissingData = true, }: MatrixHistogramQueryProps): [ boolean, UseMatrixHistogramArgs, @@ -99,6 +101,7 @@ export const useMatrixHistogram = ({ threshold, ...(isPtrIncluded != null ? { isPtrIncluded } : {}), ...(!isEmpty(docValueFields) ? { docValueFields } : {}), + ...(includeMissingData != null ? { includeMissingData } : {}), }); const { addError, addWarning } = useAppToasts(); @@ -215,6 +218,7 @@ export const useMatrixHistogram = ({ ]); useEffect(() => { + // We want to search if it is not skipped, stackByField ends with ip and include missing data if (!skip) { hostsSearch(matrixHistogramRequest); } @@ -240,3 +244,72 @@ export const useMatrixHistogram = ({ return [loading, matrixHistogramResponse, runMatrixHistogramSearch]; }; + +/* function needed to split ip histogram data requests due to elasticsearch bug https://github.com/elastic/kibana/issues/89205 + * using includeMissingData parameter to do the "missing data" query separately + **/ +export const useMatrixHistogramCombined = ( + matrixHistogramQueryProps: MatrixHistogramQueryProps +): [boolean, UseMatrixHistogramArgs] => { + const [mainLoading, mainResponse] = useMatrixHistogram({ + ...matrixHistogramQueryProps, + includeMissingData: true, + }); + + const skipMissingData = useMemo(() => !matrixHistogramQueryProps.stackByField.endsWith('.ip'), [ + matrixHistogramQueryProps.stackByField, + ]); + const [missingDataLoading, missingDataResponse] = useMatrixHistogram({ + ...matrixHistogramQueryProps, + includeMissingData: false, + skip: skipMissingData, + }); + + const combinedLoading = useMemo(() => mainLoading || missingDataLoading, [ + mainLoading, + missingDataLoading, + ]); + + const combinedResponse = useMemo(() => { + if (skipMissingData) return mainResponse; + + const { data, inspect, totalCount, refetch, buckets } = mainResponse; + const { + data: extraData, + inspect: extraInspect, + totalCount: extraTotalCount, + refetch: extraRefetch, + } = missingDataResponse; + + const combinedRefetch = () => { + refetch(); + extraRefetch(); + }; + + if (combinedLoading) { + return { + data: [], + inspect: { + dsl: [], + response: [], + }, + refetch: combinedRefetch, + totalCount: -1, + buckets: [], + }; + } + + return { + data: [...data, ...extraData], + inspect: { + dsl: [...inspect.dsl, ...extraInspect.dsl], + response: [...inspect.response, ...extraInspect.response], + }, + totalCount: totalCount + extraTotalCount, + refetch: combinedRefetch, + buckets, + }; + }, [combinedLoading, mainResponse, missingDataResponse, skipMissingData]); + + return [combinedLoading, combinedResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx index 0ec12a00d578b..0f70c6de362eb 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import '../../../common/mock/match_media'; import '../../../common/mock/react_beautiful_dnd'; -import { useMatrixHistogram } from '../../../common/containers/matrix_histogram'; +import { useMatrixHistogramCombined } from '../../../common/containers/matrix_histogram'; import { waitFor } from '@testing-library/react'; import { mockIndexPattern, TestProviders } from '../../../common/mock'; @@ -19,7 +19,7 @@ import { AlertsByCategory } from '.'; jest.mock('../../../common/components/link_to'); jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/containers/matrix_histogram', () => ({ - useMatrixHistogram: jest.fn(), + useMatrixHistogramCombined: jest.fn(), })); const from = '2020-03-31T06:00:00.000Z'; @@ -42,7 +42,7 @@ describe('Alerts by category', () => { }; describe('before loading data', () => { beforeAll(async () => { - (useMatrixHistogram as jest.Mock).mockReturnValue([ + (useMatrixHistogramCombined as jest.Mock).mockReturnValue([ false, { data: null, @@ -101,7 +101,7 @@ describe('Alerts by category', () => { describe('after loading data', () => { beforeAll(async () => { - (useMatrixHistogram as jest.Mock).mockReturnValue([ + (useMatrixHistogramCombined as jest.Mock).mockReturnValue([ false, { data: [ diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index c93d66f6cbc49..8a1d1c67174fc 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -52,6 +52,7 @@ interface Props extends Pick void; } const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ @@ -74,6 +75,7 @@ const EventsByDatasetComponent: React.FC = ({ showSpacer = true, timelineId, to, + toggleTopN, }) => { // create a unique, but stable (across re-renders) query id const uniqueQueryId = useMemo(() => `${ID}-${uuid.v4()}`, []); @@ -164,6 +166,7 @@ const EventsByDatasetComponent: React.FC = ({ headerChildren={headerContent} id={uniqueQueryId} indexNames={indexNames} + onError={toggleTopN} setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} setQuery={setQuery} showSpacer={showSpacer} diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/__mocks__/index.ts index 4b374dc0ceaa6..c361db38a6caa 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/__mocks__/index.ts @@ -308,3 +308,185 @@ export const expectedThresholdWithGroupFieldsAndCardinalityDsl = { size: 0, }, }; + +export const expectedThresholdGroupWithCardinalityDsl = { + allowNoIndices: true, + body: { + aggregations: { + eventActionGroup: { + aggs: { + cardinality_check: { + bucket_selector: { + buckets_path: { cardinalityCount: 'cardinality_count' }, + script: 'params.cardinalityCount >= 10', + }, + }, + cardinality_count: { cardinality: { field: 'agent.name' } }, + events: { + date_histogram: { + extended_bounds: { max: 1599667886215, min: 1599581486215 }, + field: '@timestamp', + fixed_interval: '2700000ms', + min_doc_count: 200, + }, + }, + }, + terms: { + order: { _count: 'desc' }, + script: { + lang: 'painless', + source: "doc['host.name'].value + ':' + doc['agent.name'].value", + }, + size: 10, + }, + }, + }, + query: { + bool: { + filter: [ + { bool: { filter: [{ match_all: {} }], must: [], must_not: [], should: [] } }, + { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2020-09-08T16:11:26.215Z', + lte: '2020-09-09T16:11:26.215Z', + }, + }, + }, + ], + }, + }, + size: 0, + }, + ignoreUnavailable: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + track_total_hits: true, +}; + +export const expectedIpIncludingMissingDataDsl = { + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + allowNoIndices: true, + ignoreUnavailable: true, + track_total_hits: true, + body: { + aggregations: { + eventActionGroup: { + terms: { + field: 'source.ip', + missing: '0.0.0.0', + value_type: 'ip', + order: { _count: 'desc' }, + size: 10, + }, + aggs: { + events: { + date_histogram: { + field: '@timestamp', + fixed_interval: '2700000ms', + min_doc_count: 0, + extended_bounds: { min: 1599581486215, max: 1599667886215 }, + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + { + bool: { + must: [], + filter: [{ match_all: {} }], + should: [], + must_not: [{ exists: { field: 'source.ip' } }], + }, + }, + { + range: { + '@timestamp': { + gte: '2020-09-08T16:11:26.215Z', + lte: '2020-09-09T16:11:26.215Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + size: 0, + }, +}; + +export const expectedIpNotIncludingMissingDataDsl = { + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + allowNoIndices: true, + ignoreUnavailable: true, + track_total_hits: true, + body: { + aggregations: { + eventActionGroup: { + terms: { field: 'source.ip', order: { _count: 'desc' }, size: 10, value_type: 'ip' }, + aggs: { + events: { + date_histogram: { + field: '@timestamp', + fixed_interval: '2700000ms', + min_doc_count: 0, + extended_bounds: { min: 1599581486215, max: 1599667886215 }, + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + { + bool: { + must: [], + filter: [{ match_all: {} }], + should: [], + must_not: [], + }, + }, + { exists: { field: 'source.ip' } }, + { + range: { + '@timestamp': { + gte: '2020-09-08T16:11:26.215Z', + lte: '2020-09-09T16:11:26.215Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + size: 0, + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.test.ts index a2d8650b3380f..1f73770f5fdb4 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.test.ts @@ -12,6 +12,9 @@ import { expectedThresholdDsl, expectedThresholdMissingFieldDsl, expectedThresholdWithCardinalityDsl, + expectedThresholdGroupWithCardinalityDsl, + expectedIpIncludingMissingDataDsl, + expectedIpNotIncludingMissingDataDsl, } from './__mocks__/'; describe('buildEventsHistogramQuery', () => { @@ -63,67 +66,25 @@ describe('buildEventsHistogramQuery', () => { cardinality: { field: ['agent.name'], value: '10' }, }, }) - ).toEqual({ - allowNoIndices: true, - body: { - aggregations: { - eventActionGroup: { - aggs: { - cardinality_check: { - bucket_selector: { - buckets_path: { cardinalityCount: 'cardinality_count' }, - script: 'params.cardinalityCount >= 10', - }, - }, - cardinality_count: { cardinality: { field: 'agent.name' } }, - events: { - date_histogram: { - extended_bounds: { max: 1599667886215, min: 1599581486215 }, - field: '@timestamp', - fixed_interval: '2700000ms', - min_doc_count: 200, - }, - }, - }, - terms: { - order: { _count: 'desc' }, - script: { - lang: 'painless', - source: "doc['host.name'].value + ':' + doc['agent.name'].value", - }, - size: 10, - }, - }, - }, - query: { - bool: { - filter: [ - { bool: { filter: [{ match_all: {} }], must: [], must_not: [], should: [] } }, - { - range: { - '@timestamp': { - format: 'strict_date_optional_time', - gte: '2020-09-08T16:11:26.215Z', - lte: '2020-09-09T16:11:26.215Z', - }, - }, - }, - ], - }, - }, - size: 0, - }, - ignoreUnavailable: true, - index: [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - track_total_hits: true, - }); + ).toEqual(expectedThresholdGroupWithCardinalityDsl); + }); + + test('builds query with stack by ip and including missing data', () => { + expect( + buildEventsHistogramQuery({ + ...mockOptions, + stackByField: 'source.ip', + }) + ).toEqual(expectedIpIncludingMissingDataDsl); + }); + + test('builds query with stack by ip and not including missing data', () => { + expect( + buildEventsHistogramQuery({ + ...mockOptions, + includeMissingData: false, + stackByField: 'source.ip', + }) + ).toEqual(expectedIpNotIncludingMissingDataDsl); }); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.ts index 15bc4694c1174..16bcb3cdbfcb1 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.ts @@ -22,9 +22,45 @@ export const buildEventsHistogramQuery = ({ defaultIndex, stackByField = 'event.action', threshold, + includeMissingData = true, }: MatrixHistogramRequestOptions) => { + const [queryFilterFirstClause, ...queryFilterClauses] = createQueryFilterClauses(filterQuery); + const stackByIpField = + stackByField != null && + showAllOthersBucket.includes(stackByField) && + stackByField.endsWith('.ip'); + const filter = [ - ...createQueryFilterClauses(filterQuery), + ...[ + { + ...queryFilterFirstClause, + bool: { + ...(queryFilterFirstClause.bool || {}), + must_not: [ + ...(queryFilterFirstClause.bool?.must_not || []), + ...(stackByIpField && includeMissingData + ? [ + { + exists: { + field: stackByField, + }, + }, + ] + : []), + ], + }, + }, + ...queryFilterClauses, + ], + ...(stackByIpField && !includeMissingData + ? [ + { + exists: { + field: stackByField, + }, + }, + ] + : []), { range: { '@timestamp': { @@ -54,7 +90,12 @@ export const buildEventsHistogramQuery = ({ const missing = stackByField != null && showAllOthersBucket.includes(stackByField) ? { - missing: stackByField?.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, + ...(includeMissingData + ? stackByField?.endsWith('.ip') + ? { missing: '0.0.0.0' } + : { missing: i18n.ALL_OTHERS } + : {}), + ...(stackByField?.endsWith('.ip') ? { value_type: 'ip' } : {}), } : {}; From c092fbfe1adac64107d2b3a2e7d43284b791eefb Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 10 May 2021 10:16:19 -0600 Subject: [PATCH 13/69] [Security Solutions] (Phase 3, part 1) Removes dependency on security_solution plugin from lists (#99431) ## Summary Removes the dependency of security_solution from the lists plugin * Removes some of the deprecated types in favor of the new kbn package * Adds a workaround in the kbn packages of removing the ?? and `a?.b?.c` typescript since kbn packages cannot transpile it * Exposes the test_utils from the kbn package ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- packages/kbn-optimizer/limits.yml | 2 +- .../src/format_errors/index.ts | 4 +- .../src/index.ts | 3 + .../src/list_types/entries_exist/index.ts | 2 +- .../src/list_types/entries_list/index.ts | 2 +- .../src/list_types/entry_match/index.ts | 2 +- .../src/list_types/entry_match_any/index.ts | 2 +- .../list_types/entry_match_wildcard/index.ts | 2 +- .../src/list_types/index.ts | 5 +- .../{operator => list_operator}/index.ts | 8 +- .../src/list_types/type/index.ts | 3 + .../src/non_empty_string_array/index.test.ts | 11 +- .../src/non_empty_string_array/index.ts | 12 +- .../src/parse_schedule_dates/index.ts | 2 +- x-pack/plugins/lists/common/constants.mock.ts | 11 +- .../build_exceptions_filter.test.ts | 68 +-- .../exceptions/build_exceptions_filter.ts | 9 +- .../plugins/lists/common/exceptions/utils.ts | 13 +- .../lists/common/format_errors.test.ts | 189 -------- x-pack/plugins/lists/common/format_errors.ts | 35 -- .../common/schemas/common/schemas.test.ts | 287 +----------- .../lists/common/schemas/common/schemas.ts | 419 +----------------- x-pack/plugins/lists/common/schemas/index.ts | 4 - .../create_endpoint_list_item_schema.test.ts | 8 +- .../create_endpoint_list_item_schema.ts | 15 +- .../create_exception_list_item_schema.test.ts | 8 +- .../create_exception_list_item_schema.ts | 22 +- .../create_exception_list_schema.test.ts | 3 +- .../request/create_exception_list_schema.ts | 17 +- .../request/create_list_item_schema.test.ts | 3 +- .../request/create_list_item_schema.ts | 3 +- .../request/create_list_schema.test.ts | 3 +- .../schemas/request/create_list_schema.ts | 12 +- .../delete_endpoint_list_item_schema.test.ts | 3 +- .../delete_endpoint_list_item_schema.ts | 3 +- .../delete_exception_list_item_schema.test.ts | 3 +- .../delete_exception_list_item_schema.ts | 4 +- .../delete_exception_list_schema.test.ts | 3 +- .../request/delete_exception_list_schema.ts | 4 +- .../request/delete_list_item_schema.test.ts | 3 +- .../request/delete_list_item_schema.ts | 3 +- .../request/delete_list_schema.test.ts | 3 +- .../schemas/request/delete_list_schema.ts | 3 +- ...export_exception_list_query_schema.test.ts | 3 +- .../export_exception_list_query_schema.ts | 3 +- .../export_list_item_query_schema.test.ts | 3 +- .../find_endpoint_list_item_schema.test.ts | 3 +- .../request/find_endpoint_list_item_schema.ts | 2 +- .../find_exception_list_item_schema.test.ts | 2 +- .../find_exception_list_item_schema.ts | 15 +- .../find_exception_list_schema.test.ts | 3 +- .../request/find_exception_list_schema.ts | 7 +- .../request/find_list_item_schema.test.ts | 2 +- .../schemas/request/find_list_item_schema.ts | 2 +- .../schemas/request/find_list_schema.test.ts | 3 +- .../schemas/request/find_list_schema.ts | 2 +- .../import_list_item_query_schema.test.ts | 3 +- .../request/import_list_item_query_schema.ts | 3 +- .../request/import_list_item_schema.test.ts | 3 +- .../request/patch_list_item_schema.test.ts | 3 +- .../schemas/request/patch_list_item_schema.ts | 3 +- .../schemas/request/patch_list_schema.test.ts | 3 +- .../schemas/request/patch_list_schema.ts | 3 +- .../read_endpoint_list_item_schema.test.ts | 3 +- .../request/read_endpoint_list_item_schema.ts | 3 +- .../read_exception_list_item_schema.test.ts | 3 +- .../read_exception_list_item_schema.ts | 4 +- .../read_exception_list_schema.test.ts | 3 +- .../request/read_exception_list_schema.ts | 4 +- .../request/read_list_item_schema.test.ts | 3 +- .../schemas/request/read_list_item_schema.ts | 3 +- .../schemas/request/read_list_schema.test.ts | 3 +- .../schemas/request/read_list_schema.ts | 3 +- .../update_endpoint_list_item_schema.test.ts | 3 +- .../update_endpoint_list_item_schema.ts | 16 +- .../update_exception_list_item_schema.test.ts | 3 +- .../update_exception_list_item_schema.ts | 19 +- .../update_exception_list_schema.test.ts | 3 +- .../request/update_exception_list_schema.ts | 11 +- .../request/update_list_item_schema.test.ts | 3 +- .../request/update_list_item_schema.ts | 3 +- .../request/update_list_schema.test.ts | 3 +- .../schemas/request/update_list_schema.ts | 3 +- .../response/acknowledge_schema.test.ts | 3 +- .../create_endpoint_list_schema.test.ts | 3 +- .../exception_list_item_schema.mock.ts | 4 - .../exception_list_item_schema.test.ts | 3 +- .../response/exception_list_item_schema.ts | 17 +- .../response/exception_list_schema.test.ts | 3 +- .../schemas/response/exception_list_schema.ts | 14 +- .../found_exception_list_item_schema.test.ts | 3 +- .../found_exception_list_schema.test.ts | 3 +- .../list_item_index_exist_schema.test.ts | 3 +- .../schemas/response/list_item_schema.test.ts | 3 +- .../schemas/response/list_item_schema.ts | 14 +- .../schemas/response/list_schema.test.ts | 3 +- .../common/schemas/response/list_schema.ts | 14 +- .../response/search_list_item_schema.test.ts | 3 +- .../common/schemas/types/comment.mock.ts | 4 +- .../common/schemas/types/comment.test.ts | 238 ---------- .../lists/common/schemas/types/comment.ts | 56 --- .../schemas/types/create_comment.mock.ts | 2 +- .../schemas/types/create_comment.test.ts | 135 ------ .../common/schemas/types/create_comment.ts | 49 -- .../types/default_comments_array.test.ts | 66 --- .../schemas/types/default_comments_array.ts | 24 - .../default_create_comments_array.test.ts | 81 ---- .../types/default_create_comments_array.ts | 28 -- .../schemas/types/default_namespace.test.ts | 62 --- .../common/schemas/types/default_namespace.ts | 25 -- .../types/default_namespace_array.test.ts | 100 ----- .../schemas/types/default_namespace_array.ts | 52 --- .../default_string_boolean_false.test.ts | 102 ----- .../types/default_string_boolean_false.ts | 37 -- .../default_update_comments_array.test.ts | 66 --- .../types/default_update_comments_array.ts | 28 -- .../schemas/types/empty_string_array.test.ts | 80 ---- .../schemas/types/empty_string_array.ts | 52 --- .../schemas/types/endpoint/entries.mock.ts | 17 - .../schemas/types/endpoint/entries.test.ts | 112 ----- .../common/schemas/types/endpoint/entries.ts | 57 --- .../types/endpoint/entry_match.mock.ts | 17 - .../types/endpoint/entry_match.test.ts | 103 ----- .../schemas/types/endpoint/entry_match.ts | 28 -- .../types/endpoint/entry_match_any.mock.ts | 17 - .../types/endpoint/entry_match_any.test.ts | 101 ----- .../schemas/types/endpoint/entry_match_any.ts | 29 -- .../types/endpoint/entry_match_wildcard.ts | 28 -- .../types/endpoint/entry_nested.mock.ts | 18 - .../types/endpoint/entry_nested.test.ts | 138 ------ .../schemas/types/endpoint/entry_nested.ts | 28 -- .../common/schemas/types/endpoint/index.ts | 8 - .../non_empty_nested_entries_array.ts | 60 --- .../common/schemas/types/entries.mock.ts | 3 +- .../common/schemas/types/entries.test.ts | 149 ------- .../lists/common/schemas/types/entries.ts | 63 --- .../common/schemas/types/entry_exists.mock.ts | 4 +- .../common/schemas/types/entry_exists.test.ts | 80 ---- .../common/schemas/types/entry_exists.ts | 27 -- .../common/schemas/types/entry_list.mock.ts | 4 +- .../common/schemas/types/entry_list.test.ts | 96 ---- .../lists/common/schemas/types/entry_list.ts | 28 -- .../common/schemas/types/entry_match.mock.ts | 4 +- .../common/schemas/types/entry_match.test.ts | 108 ----- .../lists/common/schemas/types/entry_match.ts | 28 -- .../schemas/types/entry_match_any.mock.ts | 4 +- .../schemas/types/entry_match_any.test.ts | 106 ----- .../common/schemas/types/entry_match_any.ts | 30 -- .../types/entry_match_wildcard.mock.ts | 4 +- .../types/entry_match_wildcard.test.ts | 106 ----- .../schemas/types/entry_match_wildcard.ts | 28 -- .../common/schemas/types/entry_nested.mock.ts | 3 +- .../common/schemas/types/entry_nested.test.ts | 125 ------ .../common/schemas/types/entry_nested.ts | 28 -- .../lists/common/schemas/types/index.ts | 24 - .../types/non_empty_entries_array.test.ts | 132 ------ .../schemas/types/non_empty_entries_array.ts | 48 -- .../non_empty_nested_entries_array.test.ts | 117 ----- .../types/non_empty_nested_entries_array.ts | 49 -- ...non_empty_or_nullable_string_array.test.ts | 70 --- .../non_empty_or_nullable_string_array.ts | 42 -- .../types/string_to_positive_number.ts | 37 -- .../schemas/types/update_comment.mock.ts | 4 +- .../schemas/types/update_comment.test.ts | 150 ------- .../common/schemas/types/update_comment.ts | 52 --- x-pack/plugins/lists/common/shared_exports.ts | 35 +- x-pack/plugins/lists/common/shared_imports.ts | 23 - x-pack/plugins/lists/common/test_utils.ts | 62 --- x-pack/plugins/lists/kibana.json | 2 +- x-pack/plugins/lists/public/exceptions/api.ts | 2 +- .../components/builder/entry_renderer.tsx | 2 +- .../builder/exception_item_renderer.tsx | 2 +- .../builder/exception_items_renderer.tsx | 4 +- .../exceptions/components/builder/helpers.ts | 4 +- .../public/exceptions/transforms.test.ts | 3 +- .../lists/public/exceptions/transforms.ts | 2 +- .../plugins/lists/public/exceptions/types.ts | 4 +- .../plugins/lists/public/exceptions/utils.ts | 3 +- x-pack/plugins/lists/public/lists/api.ts | 2 +- x-pack/plugins/lists/public/lists/types.ts | 3 +- .../routes/create_endpoint_list_item_route.ts | 3 +- .../routes/create_endpoint_list_route.ts | 3 +- .../create_exception_list_item_route.ts | 3 +- .../routes/create_exception_list_route.ts | 3 +- .../server/routes/create_list_index_route.ts | 3 +- .../server/routes/create_list_item_route.ts | 3 +- .../lists/server/routes/create_list_route.ts | 3 +- .../routes/delete_endpoint_list_item_route.ts | 3 +- .../delete_exception_list_item_route.ts | 3 +- .../routes/delete_exception_list_route.ts | 3 +- .../server/routes/delete_list_index_route.ts | 3 +- .../server/routes/delete_list_item_route.ts | 3 +- .../lists/server/routes/delete_list_route.ts | 4 +- .../routes/find_endpoint_list_item_route.ts | 3 +- .../routes/find_exception_list_item_route.ts | 3 +- .../routes/find_exception_list_route.ts | 3 +- .../server/routes/find_list_item_route.ts | 3 +- .../lists/server/routes/find_list_route.ts | 3 +- .../server/routes/import_list_item_route.ts | 2 +- .../server/routes/patch_list_item_route.ts | 3 +- .../lists/server/routes/patch_list_route.ts | 3 +- .../routes/read_endpoint_list_item_route.ts | 3 +- .../routes/read_exception_list_item_route.ts | 3 +- .../routes/read_exception_list_route.ts | 3 +- .../server/routes/read_list_index_route.ts | 3 +- .../server/routes/read_list_item_route.ts | 3 +- .../lists/server/routes/read_list_route.ts | 3 +- .../routes/update_endpoint_list_item_route.ts | 3 +- .../update_exception_list_item_route.ts | 3 +- .../routes/update_exception_list_route.ts | 3 +- .../server/routes/update_list_item_route.ts | 3 +- .../lists/server/routes/update_list_route.ts | 3 +- .../plugins/lists/server/routes/validate.ts | 12 +- .../server/saved_objects/migrations.test.ts | 2 +- .../lists/server/saved_objects/migrations.ts | 8 +- .../schemas}/common/get_call_cluster.mock.ts | 3 +- .../schemas}/common/get_shard.mock.ts | 0 .../server/schemas/common/schemas.test.ts | 288 ++++++++++++ .../lists/server/schemas/common/schemas.ts | 140 ++++++ .../elastic_query/create_es_bulk_type.ts | 0 .../schemas/elastic_query/index.ts | 6 +- .../index_es_list_item_schema.mock.ts | 3 +- .../index_es_list_item_schema.ts | 14 +- .../index_es_list_schema.mock.ts | 3 +- .../elastic_query/index_es_list_schema.ts | 14 +- .../update_es_list_item_schema.ts | 3 +- .../elastic_query/update_es_list_schema.ts | 3 +- .../schemas/elastic_response/index.ts | 0 .../search_es_list_item_schema.mock.ts | 5 +- .../search_es_list_item_schema.test.ts | 3 +- .../search_es_list_item_schema.ts | 22 +- .../search_es_list_schema.mock.ts | 5 +- .../search_es_list_schema.test.ts | 3 +- .../elastic_response/search_es_list_schema.ts | 14 +- .../exceptions_list_so_schema.ts | 19 +- .../schemas/saved_objects/index.ts | 0 .../create_endoint_event_filters_list.ts | 3 +- .../exception_lists/create_endpoint_list.ts | 3 +- .../create_endpoint_trusted_apps_list.ts | 3 +- .../exception_lists/create_exception_list.ts | 11 +- .../create_exception_list_item.ts | 10 +- .../exception_lists/delete_exception_list.ts | 8 +- .../delete_exception_list_item.ts | 9 +- .../delete_exception_list_items_by_list.ts | 4 +- .../exception_list_client_types.ts | 28 +- .../exception_lists/find_exception_list.ts | 4 +- .../find_exception_list_item.ts | 2 +- .../find_exception_list_items.ts | 12 +- .../exception_lists/get_exception_list.ts | 11 +- .../get_exception_list_item.ts | 11 +- .../exception_lists/update_exception_list.ts | 10 +- .../update_exception_list_item.ts | 10 +- .../server/services/exception_lists/utils.ts | 20 +- .../services/items/create_list_item.test.ts | 2 +- .../server/services/items/create_list_item.ts | 6 +- .../items/create_list_items_bulk.test.ts | 2 +- .../services/items/create_list_items_bulk.ts | 11 +- .../server/services/items/delete_list_item.ts | 3 +- .../items/delete_list_item_by_value.ts | 3 +- .../services/items/find_list_item.mock.ts | 2 +- .../services/items/find_list_item.test.ts | 4 +- .../server/services/items/find_list_item.ts | 2 +- .../services/items/get_list_item.test.ts | 2 +- .../server/services/items/get_list_item.ts | 4 +- .../services/items/get_list_item_by_value.ts | 3 +- .../items/get_list_item_by_values.test.ts | 2 +- .../services/items/get_list_item_by_values.ts | 4 +- .../lists/server/services/items/index.ts | 2 + .../items/search_list_item_by_values.test.ts | 2 +- .../items/search_list_item_by_values.ts | 4 +- .../server/services/items/update_list_item.ts | 10 +- .../items/write_lines_to_bulk_list_items.ts | 3 +- .../items/write_list_items_to_stream.test.ts | 2 +- .../items/write_list_items_to_stream.ts | 2 +- .../items/write_list_items_to_streams.mock.ts | 2 +- .../server/services/lists/create_list.test.ts | 2 +- .../server/services/lists/create_list.ts | 14 +- .../lists/create_list_if_it_does_not_exist.ts | 6 +- .../server/services/lists/delete_list.ts | 3 +- .../lists/server/services/lists/find_list.ts | 2 +- .../server/services/lists/get_list.test.ts | 2 +- .../lists/server/services/lists/get_list.ts | 4 +- .../lists/server/services/lists/index.ts | 6 +- .../services/lists/list_client_types.ts | 16 +- .../server/services/lists/update_list.ts | 14 +- .../services/utils/encode_decode_cursor.ts | 2 +- .../services/utils/find_source_type.test.ts | 6 +- .../server/services/utils/find_source_type.ts | 4 +- .../services/utils/find_source_value.test.ts | 4 +- .../services/utils/find_source_value.ts | 11 +- .../utils/get_query_filter_from_type_value.ts | 3 +- .../lists/server/services/utils/index.ts | 7 +- ..._elastic_named_search_to_list_item.test.ts | 4 +- ...sform_elastic_named_search_to_list_item.ts | 4 +- .../utils/transform_elastic_to_list.ts | 3 +- .../transform_elastic_to_list_item.test.ts | 2 +- .../utils/transform_elastic_to_list_item.ts | 4 +- ...ansform_list_item_to_elastic_query.test.ts | 2 +- .../transform_list_item_to_elastic_query.ts | 7 +- .../add_exception_modal/index.test.tsx | 3 +- .../edit_exception_modal/index.test.tsx | 2 +- .../components/exceptions/helpers.test.tsx | 3 +- .../endpoint/lib/artifacts/lists.test.ts | 2 +- .../server/endpoint/lib/artifacts/lists.ts | 2 +- .../filters/filter_events_against_list.ts | 5 +- 305 files changed, 1077 insertions(+), 5412 deletions(-) rename packages/kbn-securitysolution-io-ts-utils/src/list_types/{operator => list_operator}/index.ts (74%) rename x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts => packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.test.ts (91%) rename x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts => packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.ts (81%) delete mode 100644 x-pack/plugins/lists/common/format_errors.test.ts delete mode 100644 x-pack/plugins/lists/common/format_errors.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/comment.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/comment.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/create_comment.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/create_comment.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/default_comments_array.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/default_namespace.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/default_string_boolean_false.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/default_string_boolean_false.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/empty_string_array.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/entries.mock.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/entries.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/entries.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.mock.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.mock.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_wildcard.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.mock.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/index.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/endpoint/non_empty_nested_entries_array.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entries.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entries.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entry_exists.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entry_list.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entry_list.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entry_match.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entry_match.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entry_match_any.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/entry_nested.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/index.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/update_comment.test.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/update_comment.ts delete mode 100644 x-pack/plugins/lists/common/shared_imports.ts delete mode 100644 x-pack/plugins/lists/common/test_utils.ts rename x-pack/plugins/lists/{ => server/schemas}/common/get_call_cluster.mock.ts (94%) rename x-pack/plugins/lists/{ => server/schemas}/common/get_shard.mock.ts (100%) create mode 100644 x-pack/plugins/lists/server/schemas/common/schemas.test.ts create mode 100644 x-pack/plugins/lists/server/schemas/common/schemas.ts rename x-pack/plugins/lists/{common => server}/schemas/elastic_query/create_es_bulk_type.ts (100%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_query/index.ts (100%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_query/index_es_list_item_schema.mock.ts (90%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_query/index_es_list_item_schema.ts (86%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_query/index_es_list_schema.mock.ts (92%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_query/index_es_list_schema.ts (91%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_query/update_es_list_item_schema.ts (78%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_query/update_es_list_schema.ts (93%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_response/index.ts (100%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_response/search_es_list_item_schema.mock.ts (93%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_response/search_es_list_item_schema.test.ts (94%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_response/search_es_list_item_schema.ts (95%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_response/search_es_list_schema.mock.ts (92%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_response/search_es_list_schema.test.ts (94%) rename x-pack/plugins/lists/{common => server}/schemas/elastic_response/search_es_list_schema.ts (91%) rename x-pack/plugins/lists/{common => server}/schemas/saved_objects/exceptions_list_so_schema.ts (89%) rename x-pack/plugins/lists/{common => server}/schemas/saved_objects/index.ts (100%) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 63dd64f9202b3..08e90ed829d4a 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -46,7 +46,7 @@ pageLoadAssetSize: lens: 96624 licenseManagement: 41817 licensing: 29004 - lists: 228500 + lists: 280504 logstash: 53548 management: 46112 maps: 80000 diff --git a/packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts index 7df66dcd13596..ec37adf1221f2 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts @@ -21,7 +21,9 @@ export const formatErrors = (errors: t.Errors): string[] => { .map((entry) => entry.key) .join(','); - const nameContext = error.context.find((entry) => entry.type?.name?.length > 0); + const nameContext = error.context.find( + (entry) => entry.type != null && entry.type.name != null && entry.type.name.length > 0 + ); const suppliedValue = keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : ''; const value = isObject(error.value) ? JSON.stringify(error.value) : error.value; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/index.ts index bae90fed29dea..1a18293393af5 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/index.ts @@ -41,12 +41,14 @@ export * from './from'; export * from './id'; export * from './iso_date_string'; export * from './language'; +export * from './list_types'; export * from './max_signals'; export * from './meta'; export * from './name'; export * from './non_empty_array'; export * from './non_empty_or_nullable_string_array'; export * from './non_empty_string'; +export * from './non_empty_string_array'; export * from './normalized_ml_job_id'; export * from './only_false_allowed'; export * from './operator'; @@ -61,6 +63,7 @@ export * from './severity'; export * from './severity_mapping'; export * from './string_to_positive_number'; export * from './tags'; +export * from './test_utils'; export * from './threat'; export * from './threat_mapping'; export * from './threat_subtechnique'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts index 79c58944ea3f5..f8f1ddecc9ff9 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { operator } from '../operator'; +import { listOperator as operator } from '../list_operator'; import { NonEmptyString } from '../../non_empty_string'; export const entriesExists = t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts index f5c662a67158b..b386ca35d2bbb 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts @@ -10,7 +10,7 @@ import * as t from 'io-ts'; import { NonEmptyString } from '../../non_empty_string'; import { type } from '../type'; -import { operator } from '../operator'; +import { listOperator as operator } from '../list_operator'; export const entriesList = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts index 668da1a398093..cab6d0dd4a07f 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { NonEmptyString } from '../../non_empty_string'; -import { operator } from '../operator'; +import { listOperator as operator } from '../list_operator'; export const entriesMatch = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts index 4e7690a80f470..0add9a610f30b 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { operator } from '../operator'; +import { listOperator as operator } from '../list_operator'; import { nonEmptyOrNullableStringArray } from '../../non_empty_or_nullable_string_array'; import { NonEmptyString } from '../../non_empty_string'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts index 100b0c665d91b..aab5ba5e8e32c 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { NonEmptyString } from '../../non_empty_string'; -import { operator } from '../operator'; +import { listOperator as operator } from '../list_operator'; export const entriesMatchWildcard = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts index 652395fa651ea..9dd58e2a5a177 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts @@ -8,10 +8,11 @@ export * from './comment'; export * from './create_comment'; export * from './default_comments_array'; -export * from './default_comments_array'; +export * from './default_create_comments_array'; export * from './default_namespace'; export * from './default_namespace_array'; export * from './default_update_comments_array'; +export * from './endpoint'; export * from './entries'; export * from './entries_exist'; export * from './entries_list'; @@ -26,7 +27,7 @@ export * from './lists'; export * from './lists_default_array'; export * from './non_empty_entries_array'; export * from './non_empty_nested_entries_array'; -export * from './operator'; +export * from './list_operator'; export * from './os_type'; export * from './type'; export * from './update_comment'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/operator/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/list_operator/index.ts similarity index 74% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/operator/index.ts rename to packages/kbn-securitysolution-io-ts-utils/src/list_types/list_operator/index.ts index 7fe2b6e4d8ba7..396577d46cd72 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/operator/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/list_operator/index.ts @@ -8,14 +8,14 @@ import * as t from 'io-ts'; -export const operator = t.keyof({ excluded: null, included: null }); -export type Operator = t.TypeOf; -export enum OperatorEnum { +export const listOperator = t.keyof({ excluded: null, included: null }); +export type ListOperator = t.TypeOf; +export enum ListOperatorEnum { INCLUDED = 'included', EXCLUDED = 'excluded', } -export enum OperatorTypeEnum { +export enum ListOperatorTypeEnum { NESTED = 'nested', MATCH = 'match', MATCH_ANY = 'match_any', diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts index 0eebf2eeaace1..90a8c36eb8b31 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts @@ -37,3 +37,6 @@ export const type = t.keyof({ short: null, text: null, }); + +export const typeOrUndefined = t.union([type, t.undefined]); +export type Type = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.test.ts similarity index 91% rename from x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts rename to packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.test.ts index e4b4881d83f45..f56fa7faed2a4 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.test.ts @@ -1,16 +1,15 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { NonEmptyStringArray } from './non_empty_string_array'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { NonEmptyStringArray } from '.'; describe('non_empty_string_array', () => { test('it should FAIL validation when given "null"', () => { diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.ts similarity index 81% rename from x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts rename to packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.ts index 0afb318a6b33a..7eead15f69351 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import * as t from 'io-ts'; @@ -13,7 +14,6 @@ import { Either } from 'fp-ts/lib/Either'; * - A string that is not empty (which will be turned into an array of size 1) * - A comma separated string that can turn into an array by splitting on it * - Example input converted to output: "a,b,c" -> ["a", "b", "c"] - * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const NonEmptyStringArray = new t.Type( 'NonEmptyStringArray', @@ -37,12 +37,6 @@ export const NonEmptyStringArray = new t.Type( String ); -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ export type NonEmptyStringArray = t.OutputOf; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ export type NonEmptyStringArrayDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts index d8aa9d2939589..a2cc15d82391c 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts @@ -22,5 +22,5 @@ export const parseScheduleDates = (time: string): moment.Moment | null => { ? dateMath.parse(time) : null; - return formattedDate ?? null; + return formattedDate != null ? formattedDate : null; }; diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index 177f0a4b291d5..90f4825b97d43 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -6,10 +6,15 @@ */ import moment from 'moment'; +import { + EndpointEntriesArray, + EntriesArray, + Entry, + EntryMatch, + EntryNested, + OsTypeArray, +} from '@kbn/securitysolution-io-ts-utils'; -import { OsTypeArray } from './schemas/common'; -import { EntriesArray, Entry, EntryMatch, EntryNested } from './schemas/types'; -import { EndpointEntriesArray } from './schemas/types/endpoint'; export const DATE_NOW = '2020-04-20T15:25:31.830Z'; export const OLD_DATE_RELATIVE_TO_DATE_NOW = '2020-04-19T15:25:31.830Z'; export const USER = 'some user'; diff --git a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts index 291831777e471..fa073b3b4cfb6 100644 --- a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts +++ b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { EntryMatchAny } from '@kbn/securitysolution-io-ts-utils'; + import { getEntryMatchExcludeMock, getEntryMatchMock } from '../schemas/types/entry_match.mock'; import { getEntryMatchAnyExcludeMock, @@ -16,14 +18,10 @@ import { getEntryNestedMixedEntries, getEntryNestedMock, } from '../schemas/types/entry_nested.mock'; -import { - getExceptionListItemSchemaMock, - getExceptionListItemSchemaXMock, -} from '../schemas/response/exception_list_item_schema.mock'; -import { EntryMatchAny, ExceptionListItemSchema } from '../schemas'; +import { getExceptionListItemSchemaMock } from '../schemas/response/exception_list_item_schema.mock'; +import { ExceptionListItemSchema } from '../schemas'; import { - ExceptionItemSansLargeValueLists, buildExceptionFilter, buildExceptionItemFilter, buildExclusionClause, @@ -31,10 +29,8 @@ import { buildMatchAnyClause, buildMatchClause, buildNestedClause, - chunkExceptions, createOrClauses, } from './build_exceptions_filter'; -import { hasLargeValueList } from './utils'; const modifiedGetEntryMatchAnyMock = (): EntryMatchAny => ({ ...getEntryMatchAnyMock(), @@ -42,13 +38,6 @@ const modifiedGetEntryMatchAnyMock = (): EntryMatchAny => ({ value: ['some "host" name', 'some other host name'], }); -const getExceptionListItemsWoValueLists = (num: number): ExceptionItemSansLargeValueLists[] => { - const items = getExceptionListItemSchemaXMock(num); - return items.filter( - ({ entries }) => !hasLargeValueList(entries) - ) as ExceptionItemSansLargeValueLists[]; -}; - describe('build_exceptions_filter', () => { describe('buildExceptionFilter', () => { test('it should return undefined if no exception items', () => { @@ -431,55 +420,6 @@ describe('build_exceptions_filter', () => { }); }); - describe('chunkExceptions', () => { - test('it should NOT split a single should clause as there is nothing to split on with chunkSize 1', () => { - const exceptions = getExceptionListItemsWoValueLists(1); - const chunks = chunkExceptions(exceptions, 1); - expect(chunks).toHaveLength(1); - }); - - test('it should NOT split a single should clause as there is nothing to split on with chunkSize 2', () => { - const exceptions = getExceptionListItemsWoValueLists(1) as ExceptionItemSansLargeValueLists[]; - const chunks = chunkExceptions(exceptions, 2); - expect(chunks).toHaveLength(1); - }); - - test('it should return an empty array if no exception items passed in', () => { - const chunks = chunkExceptions([], 2); - expect(chunks).toEqual([]); - }); - - test('it should split an array of size 2 into a length 2 array with chunks on "chunkSize: 1"', () => { - const exceptions = getExceptionListItemsWoValueLists(2); - const chunks = chunkExceptions(exceptions, 1); - expect(chunks).toHaveLength(2); - }); - - test('it should split an array of size 2 into a length 4 array with chunks on "chunkSize: 1"', () => { - const exceptions = getExceptionListItemsWoValueLists(4); - const chunks = chunkExceptions(exceptions, 1); - expect(chunks).toHaveLength(4); - }); - - test('it should split an array of size 4 into a length 2 array with chunks on "chunkSize: 2"', () => { - const exceptions = getExceptionListItemsWoValueLists(4); - const chunks = chunkExceptions(exceptions, 2); - expect(chunks).toHaveLength(2); - }); - - test('it should NOT split an array of size 4 into any groups on "chunkSize: 5"', () => { - const exceptions = getExceptionListItemsWoValueLists(4); - const chunks = chunkExceptions(exceptions, 5); - expect(chunks).toHaveLength(1); - }); - - test('it should split an array of size 4 into 2 groups on "chunkSize: 3"', () => { - const exceptions = getExceptionListItemsWoValueLists(4); - const chunks = chunkExceptions(exceptions, 3); - expect(chunks).toHaveLength(2); - }); - }); - describe('createOrClauses', () => { test('it should create filter with one item if only one exception item exists', () => { const booleanFilter = createOrClauses([ diff --git a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts index 0a9753b02a612..0fa069ba51013 100644 --- a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts +++ b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts @@ -6,20 +6,19 @@ */ import { chunk } from 'lodash/fp'; - -import { Filter } from '../../../../../src/plugins/data/common'; import { - CreateExceptionListItemSchema, EntryExists, EntryMatch, EntryMatchAny, EntryNested, - ExceptionListItemSchema, entriesExists, entriesMatch, entriesMatchAny, entriesNested, -} from '../schemas'; +} from '@kbn/securitysolution-io-ts-utils'; + +import { Filter } from '../../../../../src/plugins/data/common'; +import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../schemas'; import { BooleanFilter, NestedFilter } from './types'; import { hasLargeValueList } from './utils'; diff --git a/x-pack/plugins/lists/common/exceptions/utils.ts b/x-pack/plugins/lists/common/exceptions/utils.ts index d7dc706882cd5..689687e44256a 100644 --- a/x-pack/plugins/lists/common/exceptions/utils.ts +++ b/x-pack/plugins/lists/common/exceptions/utils.ts @@ -5,20 +5,9 @@ * 2.0. */ -import { CreateExceptionListItemSchema, EntriesArray, ExceptionListItemSchema } from '../schemas'; - -export const hasLargeValueItem = ( - exceptionItems: Array -): boolean => { - return exceptionItems.some((exceptionItem) => hasLargeValueList(exceptionItem.entries)); -}; +import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); return found.length > 0; }; - -export const hasNestedEntry = (entries: EntriesArray): boolean => { - const found = entries.filter(({ type }) => type === 'nested'); - return found.length > 0; -}; diff --git a/x-pack/plugins/lists/common/format_errors.test.ts b/x-pack/plugins/lists/common/format_errors.test.ts deleted file mode 100644 index e95d9c1d5b461..0000000000000 --- a/x-pack/plugins/lists/common/format_errors.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { formatErrors } from './format_errors'; - -describe('utils', () => { - test('returns an empty error message string if there are no errors', () => { - const errors: t.Errors = []; - const output = formatErrors(errors); - expect(output).toEqual([]); - }); - - test('returns a single error message if given one', () => { - const validationError: t.ValidationError = { - context: [], - message: 'some error', - value: 'Some existing error', - }; - const errors: t.Errors = [validationError]; - const output = formatErrors(errors); - expect(output).toEqual(['some error']); - }); - - test('returns a two error messages if given two', () => { - const validationError1: t.ValidationError = { - context: [], - message: 'some error 1', - value: 'Some existing error 1', - }; - const validationError2: t.ValidationError = { - context: [], - message: 'some error 2', - value: 'Some existing error 2', - }; - const errors: t.Errors = [validationError1, validationError2]; - const output = formatErrors(errors); - expect(output).toEqual(['some error 1', 'some error 2']); - }); - - test('it filters out duplicate error messages', () => { - const validationError1: t.ValidationError = { - context: [], - message: 'some error 1', - value: 'Some existing error 1', - }; - const validationError2: t.ValidationError = { - context: [], - message: 'some error 1', - value: 'Some existing error 1', - }; - const errors: t.Errors = [validationError1, validationError2]; - const output = formatErrors(errors); - expect(output).toEqual(['some error 1']); - }); - - test('will use message before context if it is set', () => { - const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; - const validationError1: t.ValidationError = { - context, - message: 'I should be used first', - value: 'Some existing error 1', - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual(['I should be used first']); - }); - - test('will use context entry of a single string', () => { - const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; - const validationError1: t.ValidationError = { - context, - value: 'Some existing error 1', - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "some string key"']); - }); - - test('will use two context entries of two strings', () => { - const context: t.Context = ([ - { key: 'some string key 1' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - context, - value: 'Some existing error 1', - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 1,some string key 2"', - ]); - }); - - test('will filter out and not use any strings of numbers', () => { - const context: t.Context = ([ - { key: '5' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - context, - value: 'Some existing error 1', - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 2"', - ]); - }); - - test('will filter out and not use null', () => { - const context: t.Context = ([ - { key: null }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - context, - value: 'Some existing error 1', - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 2"', - ]); - }); - - test('will filter out and not use empty strings', () => { - const context: t.Context = ([ - { key: '' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - context, - value: 'Some existing error 1', - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 2"', - ]); - }); - - test('will use a name context if it cannot find a keyContext', () => { - const context: t.Context = ([ - { key: '' }, - { key: '', type: { name: 'someName' } }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - context, - value: 'Some existing error 1', - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "someName"']); - }); - - test('will return an empty string if name does not exist but type does', () => { - const context: t.Context = ([{ key: '' }, { key: '', type: {} }] as unknown) as t.Context; - const validationError1: t.ValidationError = { - context, - value: 'Some existing error 1', - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual(['Invalid value "Some existing error 1" supplied to ""']); - }); - - test('will stringify an error value', () => { - const context: t.Context = ([ - { key: '' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - context, - value: { foo: 'some error' }, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "{"foo":"some error"}" supplied to "some string key 2"', - ]); - }); -}); diff --git a/x-pack/plugins/lists/common/format_errors.ts b/x-pack/plugins/lists/common/format_errors.ts deleted file mode 100644 index 16925699b0fcf..0000000000000 --- a/x-pack/plugins/lists/common/format_errors.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { isObject } from 'lodash/fp'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts - */ -export const formatErrors = (errors: t.Errors): string[] => { - const err = errors.map((error) => { - if (error.message != null) { - return error.message; - } else { - const keyContext = error.context - .filter( - (entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== '' - ) - .map((entry) => entry.key) - .join(','); - - const nameContext = error.context.find((entry) => entry.type?.name?.length > 0); - const suppliedValue = - keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : ''; - const value = isObject(error.value) ? JSON.stringify(error.value) : error.value; - return `Invalid value "${value}" supplied to "${suppliedValue}"`; - } - }); - - return [...new Set(err)]; -}; diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts index 9f3abb9259f6c..2b007f01b56eb 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts @@ -7,31 +7,19 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; - import { - EsDataTypeGeoPoint, - EsDataTypeGeoPointRange, - EsDataTypeRange, - EsDataTypeRangeTerm, - EsDataTypeSingle, - EsDataTypeUnion, ExceptionListTypeEnum, - OperatorEnum, + ListOperatorEnum as OperatorEnum, Type, - esDataTypeGeoPoint, - esDataTypeGeoPointRange, - esDataTypeRange, - esDataTypeRangeTerm, - esDataTypeSingle, - esDataTypeUnion, + exactCheck, exceptionListType, - operator, + foldLeftRight, + getPaths, + listOperator as operator, osType, osTypeArrayOrUndefined, type, -} from './schemas'; +} from '@kbn/securitysolution-io-ts-utils'; describe('Common schemas', () => { describe('operator', () => { @@ -120,269 +108,6 @@ describe('Common schemas', () => { }); }); - describe('esDataTypeRange', () => { - test('it will work with a given gte, lte range', () => { - const payload: EsDataTypeRange = { gte: '127.0.0.1', lte: '127.0.0.1' }; - const decoded = esDataTypeRange.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will give an error if given an extra madeup value', () => { - const payload: EsDataTypeRange & { madeupvalue: string } = { - gte: '127.0.0.1', - lte: '127.0.0.1', - madeupvalue: 'something', - }; - const decoded = esDataTypeRange.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); - expect(message.schema).toEqual({}); - }); - }); - - describe('esDataTypeRangeTerm', () => { - test('it will work with a date_range', () => { - const payload: EsDataTypeRangeTerm = { date_range: { gte: '2015', lte: '2017' } }; - const decoded = esDataTypeRangeTerm.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will give an error if given an extra madeup value for date_range', () => { - const payload: EsDataTypeRangeTerm & { madeupvalue: string } = { - date_range: { gte: '2015', lte: '2017' }, - madeupvalue: 'something', - }; - const decoded = esDataTypeRangeTerm.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); - expect(message.schema).toEqual({}); - }); - - test('it will work with a double_range', () => { - const payload: EsDataTypeRangeTerm = { double_range: { gte: '2015', lte: '2017' } }; - const decoded = esDataTypeRangeTerm.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will give an error if given an extra madeup value for double_range', () => { - const payload: EsDataTypeRangeTerm & { madeupvalue: string } = { - double_range: { gte: '2015', lte: '2017' }, - madeupvalue: 'something', - }; - const decoded = esDataTypeRangeTerm.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); - expect(message.schema).toEqual({}); - }); - - test('it will work with a float_range', () => { - const payload: EsDataTypeRangeTerm = { float_range: { gte: '2015', lte: '2017' } }; - const decoded = esDataTypeRangeTerm.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will give an error if given an extra madeup value for float_range', () => { - const payload: EsDataTypeRangeTerm & { madeupvalue: string } = { - float_range: { gte: '2015', lte: '2017' }, - madeupvalue: 'something', - }; - const decoded = esDataTypeRangeTerm.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); - expect(message.schema).toEqual({}); - }); - - test('it will work with a integer_range', () => { - const payload: EsDataTypeRangeTerm = { integer_range: { gte: '2015', lte: '2017' } }; - const decoded = esDataTypeRangeTerm.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will give an error if given an extra madeup value for integer_range', () => { - const payload: EsDataTypeRangeTerm & { madeupvalue: string } = { - integer_range: { gte: '2015', lte: '2017' }, - madeupvalue: 'something', - }; - const decoded = esDataTypeRangeTerm.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); - expect(message.schema).toEqual({}); - }); - - test('it will work with a ip_range', () => { - const payload: EsDataTypeRangeTerm = { ip_range: { gte: '127.0.0.1', lte: '127.0.0.2' } }; - const decoded = esDataTypeRangeTerm.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will work with a ip_range as a CIDR', () => { - const payload: EsDataTypeRangeTerm = { ip_range: '127.0.0.1/16' }; - const decoded = esDataTypeRangeTerm.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will give an error if given an extra madeup value for ip_range', () => { - const payload: EsDataTypeRangeTerm & { madeupvalue: string } = { - ip_range: { gte: '127.0.0.1', lte: '127.0.0.2' }, - madeupvalue: 'something', - }; - const decoded = esDataTypeRangeTerm.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); - expect(message.schema).toEqual({}); - }); - - test('it will work with a long_range', () => { - const payload: EsDataTypeRangeTerm = { long_range: { gte: '2015', lte: '2017' } }; - const decoded = esDataTypeRangeTerm.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will give an error if given an extra madeup value for long_range', () => { - const payload: EsDataTypeRangeTerm & { madeupvalue: string } = { - long_range: { gte: '2015', lte: '2017' }, - madeupvalue: 'something', - }; - const decoded = esDataTypeRangeTerm.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); - expect(message.schema).toEqual({}); - }); - }); - - describe('esDataTypeGeoPointRange', () => { - test('it will work with a given lat, lon range', () => { - const payload: EsDataTypeGeoPointRange = { lat: '20', lon: '30' }; - const decoded = esDataTypeGeoPointRange.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will give an error if given an extra madeup value', () => { - const payload: EsDataTypeGeoPointRange & { madeupvalue: string } = { - lat: '20', - lon: '30', - madeupvalue: 'something', - }; - const decoded = esDataTypeGeoPointRange.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); - expect(message.schema).toEqual({}); - }); - }); - - describe('esDataTypeGeoPoint', () => { - test('it will work with a given lat, lon range', () => { - const payload: EsDataTypeGeoPoint = { geo_point: { lat: '127.0.0.1', lon: '127.0.0.1' } }; - const decoded = esDataTypeGeoPoint.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will work with a WKT (Well known text)', () => { - const payload: EsDataTypeGeoPoint = { geo_point: 'POINT (30 10)' }; - const decoded = esDataTypeGeoPoint.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will give an error if given an extra madeup value', () => { - const payload: EsDataTypeGeoPoint & { madeupvalue: string } = { - geo_point: 'POINT (30 10)', - madeupvalue: 'something', - }; - const decoded = esDataTypeGeoPoint.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); - expect(message.schema).toEqual({}); - }); - }); - - describe('esDataTypeSingle', () => { - test('it will work with single type', () => { - const payload: EsDataTypeSingle = { boolean: 'true' }; - const decoded = esDataTypeSingle.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will not work with a madeup value', () => { - const payload: EsDataTypeSingle & { madeupValue: 'madeup' } = { - boolean: 'true', - madeupValue: 'madeup', - }; - const decoded = esDataTypeSingle.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupValue"']); - expect(message.schema).toEqual({}); - }); - }); - - describe('esDataTypeUnion', () => { - test('it will work with a regular union', () => { - const payload: EsDataTypeUnion = { boolean: 'true' }; - const decoded = esDataTypeUnion.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will not work with a madeup value', () => { - const payload: EsDataTypeUnion & { madeupValue: 'madeupValue' } = { - boolean: 'true', - madeupValue: 'madeupValue', - }; - const decoded = esDataTypeUnion.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupValue"']); - expect(message.schema).toEqual({}); - }); - }); - describe('osType', () => { test('it will validate a correct osType', () => { const payload = 'windows'; diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index f223d56eb15cb..eb84ee07981f3 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -8,49 +8,18 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; - -import { DefaultNamespace } from '../types/default_namespace'; -import { DefaultArray, DefaultStringArray, NonEmptyString } from '../../shared_imports'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const name = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Name = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const nameOrUndefined = t.union([name, t.undefined]); +import { DefaultNamespace, NonEmptyString } from '@kbn/securitysolution-io-ts-utils'; /** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils + * @deprecated Directly use the type from the package and not from here */ -export type NameOrUndefined = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const description = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Description = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const descriptionOrUndefined = t.union([description, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type DescriptionOrUndefined = t.TypeOf; +export { + Type, + OsType, + OsTypeArray, + listOperator as operator, + NonEmptyEntriesArray, +} from '@kbn/securitysolution-io-ts-utils'; export const list_id = NonEmptyString; export type ListId = t.TypeOf; @@ -59,307 +28,7 @@ export type ListIdOrUndefined = t.TypeOf; export const item = t.string; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const created_at = t.string; // TODO: Make this into an ISO Date string check - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const updated_at = t.string; // TODO: Make this into an ISO Date string check - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const updated_by = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const created_by = t.string; - export const file = t.object; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const id = NonEmptyString; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Id = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const idOrUndefined = t.union([id, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type IdOrUndefined = t.TypeOf; - -export const binary = t.string; -export const binaryOrUndefined = t.union([binary, t.undefined]); - -export const boolean = t.string; -export const booleanOrUndefined = t.union([boolean, t.undefined]); - -export const byte = t.string; -export const byteOrUndefined = t.union([byte, t.undefined]); - -export const date = t.string; -export const dateOrUndefined = t.union([date, t.undefined]); - -export const date_nanos = t.string; -export const dateNanosOrUndefined = t.union([date_nanos, t.undefined]); - -export const double = t.string; -export const doubleOrUndefined = t.union([double, t.undefined]); - -export const float = t.string; -export const floatOrUndefined = t.union([float, t.undefined]); - -export const geo_shape = t.string; -export const geoShapeOrUndefined = t.union([geo_shape, t.undefined]); - -export const half_float = t.string; -export const halfFloatOrUndefined = t.union([half_float, t.undefined]); - -export const integer = t.string; -export const integerOrUndefined = t.union([integer, t.undefined]); - -export const ip = t.string; -export const ipOrUndefined = t.union([ip, t.undefined]); - -export const keyword = t.string; -export const keywordOrUndefined = t.union([keyword, t.undefined]); - -export const text = t.string; -export const textOrUndefined = t.union([text, t.undefined]); - -export const long = t.string; -export const longOrUndefined = t.union([long, t.undefined]); - -export const shape = t.string; -export const shapeOrUndefined = t.union([shape, t.undefined]); - -export const short = t.string; -export const shortOrUndefined = t.union([short, t.undefined]); - -export const value = t.string; -export const valueOrUndefined = t.union([value, t.undefined]); - -export const tie_breaker_id = t.string; // TODO: Use UUID for this instead of a string for validation -export const _index = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const type = t.keyof({ - binary: null, - boolean: null, - byte: null, - date: null, - date_nanos: null, - date_range: null, - double: null, - double_range: null, - float: null, - float_range: null, - geo_point: null, - geo_shape: null, - half_float: null, - integer: null, - integer_range: null, - ip: null, - ip_range: null, - keyword: null, - long: null, - long_range: null, - shape: null, - short: null, - text: null, -}); - -export const typeOrUndefined = t.union([type, t.undefined]); -export type Type = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const meta = t.object; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Meta = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const metaOrUndefined = t.union([meta, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type MetaOrUndefined = t.TypeOf; - -export const esDataTypeRange = t.exact(t.type({ gte: t.string, lte: t.string })); - -export const date_range = esDataTypeRange; -export const dateRangeOrUndefined = t.union([date_range, t.undefined]); - -export const double_range = esDataTypeRange; -export const doubleRangeOrUndefined = t.union([double_range, t.undefined]); - -export const float_range = esDataTypeRange; -export const floatRangeOrUndefined = t.union([float_range, t.undefined]); - -export const integer_range = esDataTypeRange; -export const integerRangeOrUndefined = t.union([integer_range, t.undefined]); - -// ip_range can be just a CIDR value as a range -export const ip_range = t.union([esDataTypeRange, t.string]); -export const ipRangeOrUndefined = t.union([ip_range, t.undefined]); - -export const long_range = esDataTypeRange; -export const longRangeOrUndefined = t.union([long_range, t.undefined]); - -export type EsDataTypeRange = t.TypeOf; - -export const esDataTypeRangeTerm = t.union([ - t.exact(t.type({ date_range })), - t.exact(t.type({ double_range })), - t.exact(t.type({ float_range })), - t.exact(t.type({ integer_range })), - t.exact(t.type({ ip_range })), - t.exact(t.type({ long_range })), -]); - -export type EsDataTypeRangeTerm = t.TypeOf; - -export const esDataTypeGeoPointRange = t.exact(t.type({ lat: t.string, lon: t.string })); -export type EsDataTypeGeoPointRange = t.TypeOf; - -export const geo_point = t.union([esDataTypeGeoPointRange, t.string]); -export type GeoPoint = t.TypeOf; - -export const geoPointOrUndefined = t.union([geo_point, t.undefined]); - -export const esDataTypeGeoPoint = t.exact(t.type({ geo_point })); -export type EsDataTypeGeoPoint = t.TypeOf; - -export const esDataTypeGeoShape = t.union([ - t.exact(t.type({ geo_shape: t.string })), - t.exact(t.type({ shape: t.string })), -]); - -export type EsDataTypeGeoShape = t.TypeOf; - -export const esDataTypeSingle = t.union([ - t.exact(t.type({ binary })), - t.exact(t.type({ boolean })), - t.exact(t.type({ byte })), - t.exact(t.type({ date })), - t.exact(t.type({ date_nanos })), - t.exact(t.type({ double })), - t.exact(t.type({ float })), - t.exact(t.type({ half_float })), - t.exact(t.type({ integer })), - t.exact(t.type({ ip })), - t.exact(t.type({ keyword })), - t.exact(t.type({ long })), - t.exact(t.type({ short })), - t.exact(t.type({ text })), -]); - -export type EsDataTypeSingle = t.TypeOf; - -export const esDataTypeUnion = t.union([ - esDataTypeRangeTerm, - esDataTypeGeoPoint, - esDataTypeGeoShape, - esDataTypeSingle, -]); - -export type EsDataTypeUnion = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const tags = DefaultStringArray; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Tags = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const tagsOrUndefined = t.union([tags, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type TagsOrUndefined = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const exceptionListType = t.keyof({ - detection: null, - endpoint: null, - endpoint_events: null, -}); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const exceptionListTypeOrUndefined = t.union([exceptionListType, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ExceptionListType = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ExceptionListTypeOrUndefined = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export enum ExceptionListTypeEnum { - DETECTION = 'detection', - ENDPOINT = 'endpoint', - ENDPOINT_EVENTS = 'endpoint_events', -} - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const exceptionListItemType = t.keyof({ simple: null }); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const exceptionListItemTypeOrUndefined = t.union([exceptionListItemType, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ExceptionListItemType = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ExceptionListItemTypeOrUndefined = t.TypeOf; - export const list_type = t.keyof({ item: null, list: null }); export type ListType = t.TypeOf; @@ -404,41 +73,6 @@ export type CursorOrUndefined = t.TypeOf; export const namespace_type = DefaultNamespace; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const operatorIncluded = t.keyof({ included: null }); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const operator = t.keyof({ excluded: null, included: null }); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Operator = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export enum OperatorEnum { - INCLUDED = 'included', - EXCLUDED = 'excluded', -} - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export enum OperatorTypeEnum { - NESTED = 'nested', - MATCH = 'match', - MATCH_ANY = 'match_any', - WILDCARD = 'wildcard', - EXISTS = 'exists', - LIST = 'list', -} - export const serializer = t.string; export type Serializer = t.TypeOf; @@ -467,36 +101,7 @@ export type Immutable = t.TypeOf; export const immutableOrUndefined = t.union([immutable, t.undefined]); export type ImmutableOrUndefined = t.TypeOf; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const osType = t.keyof({ - linux: null, - macos: null, - windows: null, -}); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type OsType = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const osTypeArray = DefaultArray(osType); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type OsTypeArray = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const osTypeArrayOrUndefined = t.union([osTypeArray, t.undefined]); +export const value = t.string; +export const valueOrUndefined = t.union([value, t.undefined]); -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type OsTypeArrayOrUndefined = t.OutputOf; +export const tie_breaker_id = t.string; // TODO: Use UUID for this instead of a string for validation diff --git a/x-pack/plugins/lists/common/schemas/index.ts b/x-pack/plugins/lists/common/schemas/index.ts index 0e6312e642c58..7731d555a5dd3 100644 --- a/x-pack/plugins/lists/common/schemas/index.ts +++ b/x-pack/plugins/lists/common/schemas/index.ts @@ -6,9 +6,5 @@ */ export * from './common'; -export * from './elastic_query'; -export * from './elastic_response'; export * from './request'; export * from './response'; -export * from './saved_objects'; -export * from './types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts index 728867b06bca5..30f3acc8a164a 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts @@ -7,11 +7,15 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; +import { + CommentsArray, + exactCheck, + foldLeftRight, + getPaths, +} from '@kbn/securitysolution-io-ts-utils'; -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; -import { CommentsArray } from '../types'; import { CreateEndpointListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts index 6adffd362846d..af58c61dbaf9f 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts @@ -6,23 +6,24 @@ */ import * as t from 'io-ts'; - import { - ItemId, + CreateCommentsArray, + DefaultCreateCommentsArray, + DefaultUuid, + EntriesArray, OsTypeArray, Tags, description, exceptionListItemType, meta, name, + nonEmptyEndpointEntriesArray, osTypeArrayOrUndefined, tags, -} from '../common/schemas'; +} from '@kbn/securitysolution-io-ts-utils'; + +import { ItemId } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { CreateCommentsArray, DefaultCreateCommentsArray } from '../types'; -import { nonEmptyEndpointEntriesArray } from '../types/endpoint'; -import { EntriesArray } from '../types/entries'; -import { DefaultUuid } from '../../shared_imports'; export const createEndpointListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts index d21ed7039694c..1bb58d6195e7c 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts @@ -7,11 +7,15 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; +import { + CommentsArray, + exactCheck, + foldLeftRight, + getPaths, +} from '@kbn/securitysolution-io-ts-utils'; -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; -import { CommentsArray } from '../types'; import { CreateExceptionListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index 77f4f15d62038..da5630ef3f002 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -6,29 +6,25 @@ */ import * as t from 'io-ts'; - import { - ItemId, + CreateCommentsArray, + DefaultCreateCommentsArray, + DefaultUuid, + EntriesArray, + NamespaceType, OsTypeArray, Tags, description, exceptionListItemType, - list_id, meta, name, - namespace_type, + nonEmptyEntriesArray, osTypeArrayOrUndefined, tags, -} from '../common/schemas'; +} from '@kbn/securitysolution-io-ts-utils'; + +import { ItemId, list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { - CreateCommentsArray, - DefaultCreateCommentsArray, - NamespaceType, - nonEmptyEntriesArray, -} from '../types'; -import { EntriesArray } from '../types/entries'; -import { DefaultUuid } from '../../shared_imports'; export const createExceptionListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts index ceefec34c0462..e6f29bc02702d 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { CreateExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts index 0f67f7690d4b1..42955ddbd7017 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts @@ -6,26 +6,23 @@ */ import * as t from 'io-ts'; - import { - ListId, + DefaultUuid, + DefaultVersionNumber, + DefaultVersionNumberDecoded, + NamespaceType, OsTypeArray, Tags, description, exceptionListType, meta, name, - namespace_type, osTypeArrayOrUndefined, tags, -} from '../common/schemas'; +} from '@kbn/securitysolution-io-ts-utils'; + +import { ListId, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { - DefaultUuid, - DefaultVersionNumber, - DefaultVersionNumberDecoded, -} from '../../shared_imports'; -import { NamespaceType } from '../types'; export const createExceptionListSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts index 8ae65b32cfecb..99fd1f28dcae3 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getCreateListItemSchemaMock } from './create_list_item_schema.mock'; import { CreateListItemSchema, createListItemSchema } from './create_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts index 0a23de9b103fa..867b441960a2c 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts @@ -6,8 +6,9 @@ */ import * as t from 'io-ts'; +import { id, meta } from '@kbn/securitysolution-io-ts-utils'; -import { id, list_id, meta, value } from '../common/schemas'; +import { list_id, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const createListItemSchema = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts index fbc9094ec7335..d183465a333af 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { CreateListSchema, createListSchema } from './create_list_schema'; import { getCreateListSchemaMock } from './create_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts index dc77dcb8b91a3..8ac36cc3ad28e 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts @@ -6,10 +6,18 @@ */ import * as t from 'io-ts'; +import { + DefaultVersionNumber, + DefaultVersionNumberDecoded, + description, + id, + meta, + name, + type, +} from '@kbn/securitysolution-io-ts-utils'; -import { description, deserializer, id, meta, name, serializer, type } from '../common/schemas'; +import { deserializer, serializer } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { DefaultVersionNumber, DefaultVersionNumberDecoded } from '../../shared_imports'; export const createListSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts index 8acb20905b616..11c3eaf866520 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { DeleteEndpointListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts index 12d983fe9954b..b8ff0834e8fb8 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts @@ -6,8 +6,9 @@ */ import * as t from 'io-ts'; +import { id } from '@kbn/securitysolution-io-ts-utils'; -import { id, item_id } from '../common/schemas'; +import { item_id } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const deleteEndpointListItemSchema = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts index fc500b8821232..63a1e29419760 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { DeleteExceptionListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts index 3a92823b319e8..cc188bf52d75c 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts @@ -6,9 +6,9 @@ */ import * as t from 'io-ts'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; -import { id, item_id, namespace_type } from '../common/schemas'; -import { NamespaceType } from '../types'; +import { item_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const deleteExceptionListItemSchema = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts index 0ee6e392f1f5c..ea591f74b6b15 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { DeleteExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts index 8258b4480aec8..b816c08beb363 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts @@ -6,9 +6,9 @@ */ import * as t from 'io-ts'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; -import { id, list_id, namespace_type } from '../common/schemas'; -import { NamespaceType } from '../types'; +import { list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const deleteExceptionListSchema = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts index f2e05d3c3cb7b..350243e10e2b9 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { DeleteListItemSchema, deleteListItemSchema } from './delete_list_item_schema'; import { getDeleteListItemSchemaMock } from './delete_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts index 2c5dd5bbeac08..5b4aa63d2d090 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts @@ -6,8 +6,9 @@ */ import * as t from 'io-ts'; +import { id } from '@kbn/securitysolution-io-ts-utils'; -import { id, list_id, valueOrUndefined } from '../common/schemas'; +import { list_id, valueOrUndefined } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const deleteListItemSchema = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts index 9efcffb771eb8..92a33c73ba3be 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { DeleteListSchema, deleteListSchema } from './delete_list_schema'; import { getDeleteListSchemaMock } from './delete_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts index a4045e6c5a812..003dfdc6bd466 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts @@ -6,10 +6,9 @@ */ import * as t from 'io-ts'; +import { DefaultStringBooleanFalse, id } from '@kbn/securitysolution-io-ts-utils'; -import { id } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { DefaultStringBooleanFalse } from '../types/default_string_boolean_false'; export const deleteListSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.test.ts index bb9698432f77a..06b432e74342d 100644 --- a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { ExportExceptionListQuerySchema, diff --git a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts index 2c45f2804ba93..d3c18f0d1c485 100644 --- a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts @@ -6,8 +6,9 @@ */ import * as t from 'io-ts'; +import { id } from '@kbn/securitysolution-io-ts-utils'; -import { id, list_id, namespace_type } from '../common/schemas'; +import { list_id, namespace_type } from '../common/schemas'; export const exportExceptionListQuerySchema = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts index b45767ec20749..2ac69e0c281b3 100644 --- a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { ExportListItemQuerySchema, diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts index 985311012eaf5..bd9a2a0bcb9e2 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getFindEndpointListItemSchemaDecodedMock, diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts index 978f16ed1acd9..13f45a070b2b7 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts @@ -6,10 +6,10 @@ */ import * as t from 'io-ts'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-utils'; import { filter, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { StringToPositiveNumber } from '../types/string_to_positive_number'; export const findEndpointListItemSchema = t.exact( t.partial({ diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts index 712209efe0ac9..d3a594e052c01 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts @@ -7,8 +7,8 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { LIST_ID } from '../../constants.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts index baaa3f571fb5b..4abceb4b3592d 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts @@ -6,16 +6,17 @@ */ import * as t from 'io-ts'; - -import { sort_field, sort_order } from '../common/schemas'; -import { RequiredKeepUndefined } from '../../types'; -import { StringToPositiveNumber } from '../types/string_to_positive_number'; import { DefaultNamespaceArray, DefaultNamespaceArrayTypeDecoded, -} from '../types/default_namespace_array'; -import { NonEmptyStringArray } from '../types/non_empty_string_array'; -import { EmptyStringArray, EmptyStringArrayDecoded } from '../types/empty_string_array'; + EmptyStringArray, + EmptyStringArrayDecoded, + NonEmptyStringArray, + StringToPositiveNumber, +} from '@kbn/securitysolution-io-ts-utils'; + +import { sort_field, sort_order } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; export const findExceptionListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts index ae07fecef1314..b1ec33878bd2a 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getFindExceptionListSchemaDecodedMock, diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts index 2d5eda1c2ea8e..ea5b5c5aafdb6 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts @@ -6,11 +6,14 @@ */ import * as t from 'io-ts'; +import { + DefaultNamespaceArray, + NamespaceTypeArray, + StringToPositiveNumber, +} from '@kbn/securitysolution-io-ts-utils'; import { filter, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { StringToPositiveNumber } from '../types/string_to_positive_number'; -import { DefaultNamespaceArray, NamespaceTypeArray } from '../types/default_namespace_array'; export const findExceptionListSchema = t.exact( t.partial({ diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts index dc0d47c080850..7d298c3bdcb1e 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts @@ -7,8 +7,8 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { LIST_ID } from '../../constants.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts index 434efd55bbcf0..6adf53d0eda86 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts @@ -6,10 +6,10 @@ */ import * as t from 'io-ts'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-utils'; import { cursor, filter, list_id, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { StringToPositiveNumber } from '../types/string_to_positive_number'; export const findListItemSchema = t.intersection([ t.exact(t.type({ list_id })), diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts index 70c952d48335e..a700c88618d60 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getFindListSchemaDecodedMock, getFindListSchemaMock } from './find_list_schema.mock'; import { FindListSchemaEncoded, findListSchema } from './find_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts index eae855a65bcef..bf6a68d97a58e 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts @@ -6,9 +6,9 @@ */ import * as t from 'io-ts'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-utils'; import { cursor, filter, sort_field, sort_order } from '../common/schemas'; -import { StringToPositiveNumber } from '../types/string_to_positive_number'; import { RequiredKeepUndefined } from '../../types'; export const findListSchema = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts index 5e043359f46a2..c00609e66af5b 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { ImportListItemQuerySchema, diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts index a7dd312f1ed80..85644ff556443 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts @@ -6,9 +6,10 @@ */ import * as t from 'io-ts'; +import { type } from '@kbn/securitysolution-io-ts-utils'; import { RequiredKeepUndefined } from '../../types'; -import { deserializer, list_id, serializer, type } from '../common/schemas'; +import { deserializer, list_id, serializer } from '../common/schemas'; export const importListItemQuerySchema = t.exact( t.partial({ deserializer, list_id, serializer, type }) diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts index 6837777ee5c8e..08298a505fa7c 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { ImportListItemSchema, importListItemSchema } from './import_list_item_schema'; import { getImportListItemSchemaMock } from './import_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts index 2d272e0b5aab3..2ec903eef1a9d 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getPathListItemSchemaMock } from './patch_list_item_schema.mock'; import { PatchListItemSchema, patchListItemSchema } from './patch_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts index 4c7615c5c0bce..edea4f161f248 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts @@ -6,8 +6,9 @@ */ import * as t from 'io-ts'; +import { id, meta } from '@kbn/securitysolution-io-ts-utils'; -import { _version, id, meta, value } from '../common/schemas'; +import { _version, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const patchListItemSchema = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts index 046741c525d2b..7c0e535aed2c2 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getPathListSchemaMock } from './patch_list_schema.mock'; import { PatchListSchema, patchListSchema } from './patch_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts index 09fe5075b393d..144bf9c0f28a0 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts @@ -6,8 +6,9 @@ */ import * as t from 'io-ts'; +import { description, id, meta, name } from '@kbn/securitysolution-io-ts-utils'; -import { _version, description, id, meta, name, version } from '../common/schemas'; +import { _version, version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const patchListSchema = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts index 9d3c6edd6a197..1c474db0d0bb7 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getReadEndpointListItemSchemaMock } from './read_endpoint_list_item_schema.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts index a3d46fdc4eed9..116c70012c17e 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts @@ -6,8 +6,9 @@ */ import * as t from 'io-ts'; +import { id } from '@kbn/securitysolution-io-ts-utils'; -import { id, item_id } from '../common/schemas'; +import { item_id } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const readEndpointListItemSchema = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts index bef312fffa6ac..8b713dd38c4f2 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getReadExceptionListItemSchemaMock } from './read_exception_list_item_schema.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts index 71e7de9feb81c..a0bd46b30d2f6 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts @@ -6,10 +6,10 @@ */ import * as t from 'io-ts'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; -import { id, item_id, namespace_type } from '../common/schemas'; +import { item_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { NamespaceType } from '../types'; export const readExceptionListItemSchema = t.exact( t.partial({ diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts index 5fd87ff24d3a7..031e3d6efb261 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getReadExceptionListSchemaMock } from './read_exception_list_schema.mock'; import { ReadExceptionListSchema, readExceptionListSchema } from './read_exception_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts index d0064b6463159..fc8a6ee43a5a2 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts @@ -6,10 +6,10 @@ */ import * as t from 'io-ts'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; -import { id, list_id, namespace_type } from '../common/schemas'; +import { list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { NamespaceType } from '../types'; export const readExceptionListSchema = t.exact( t.partial({ diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts index 4485ad24b8e3c..18af60f9d9d56 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getReadListItemSchemaMock } from './read_list_item_schema.mock'; import { ReadListItemSchema, readListItemSchema } from './read_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts index 07bc1295a7b0c..450719f42ad4a 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts @@ -6,8 +6,9 @@ */ import * as t from 'io-ts'; +import { id } from '@kbn/securitysolution-io-ts-utils'; -import { id, list_id, value } from '../common/schemas'; +import { list_id, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const readListItemSchema = t.exact(t.partial({ id, list_id, value })); diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts index 92a45466b339e..e404e99f65218 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getReadListSchemaMock } from './read_list_schema.mock'; import { ReadListSchema, readListSchema } from './read_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts index 989b35593422f..e07e2de1a4b80 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts @@ -6,8 +6,7 @@ */ import * as t from 'io-ts'; - -import { id } from '../common/schemas'; +import { id } from '@kbn/securitysolution-io-ts-utils'; export const readListSchema = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts index d39f8864cd68f..b5bd8caea8f85 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { UpdateEndpointListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts index 29ccb5e8e7831..d9e602419d61d 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts @@ -6,26 +6,24 @@ */ import * as t from 'io-ts'; - import { + DefaultUpdateCommentsArray, + EntriesArray, OsTypeArray, Tags, - _version, + UpdateCommentsArray, description, exceptionListItemType, id, meta, name, + nonEmptyEntriesArray, osTypeArrayOrUndefined, tags, -} from '../common/schemas'; +} from '@kbn/securitysolution-io-ts-utils'; + +import { _version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { - DefaultUpdateCommentsArray, - EntriesArray, - UpdateCommentsArray, - nonEmptyEntriesArray, -} from '../types'; export const updateEndpointListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts index 80f700aa8a4e3..efcb4ecde1cbb 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { UpdateExceptionListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts index 9c892f109dcfd..f3b87c5ff5925 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts @@ -6,28 +6,25 @@ */ import * as t from 'io-ts'; - import { + DefaultUpdateCommentsArray, + EntriesArray, + NamespaceType, OsTypeArray, Tags, - _version, + UpdateCommentsArray, description, exceptionListItemType, id, meta, name, - namespace_type, + nonEmptyEntriesArray, osTypeArrayOrUndefined, tags, -} from '../common/schemas'; +} from '@kbn/securitysolution-io-ts-utils'; + +import { _version, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { - DefaultUpdateCommentsArray, - EntriesArray, - NamespaceType, - UpdateCommentsArray, - nonEmptyEntriesArray, -} from '../types'; export const updateExceptionListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts index b782de62d736c..30966f8eafef3 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { UpdateExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts index 28933d015cb4f..c8b354eff4d9e 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts @@ -6,24 +6,21 @@ */ import * as t from 'io-ts'; - import { + NamespaceType, OsTypeArray, Tags, - _version, description, exceptionListType, id, - list_id, meta, name, - namespace_type, osTypeArrayOrUndefined, tags, - version, -} from '../common/schemas'; +} from '@kbn/securitysolution-io-ts-utils'; + +import { _version, list_id, namespace_type, version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { NamespaceType } from '../types'; export const updateExceptionListSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts index c643719e3f2f3..2775abd1ee8d0 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { UpdateListItemSchema, updateListItemSchema } from './update_list_item_schema'; import { getUpdateListItemSchemaMock } from './update_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts index 8266f6a2bed44..84916f15a59f6 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts @@ -6,8 +6,9 @@ */ import * as t from 'io-ts'; +import { id, meta } from '@kbn/securitysolution-io-ts-utils'; -import { _version, id, meta, value } from '../common/schemas'; +import { _version, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const updateListItemSchema = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_list_schema.test.ts index 8cfe7c2944414..b20aa4d774938 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { UpdateListSchema, updateListSchema } from './update_list_schema'; import { getUpdateListSchemaMock } from './update_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts index db23e53cb5792..6f520d399d577 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts @@ -6,8 +6,9 @@ */ import * as t from 'io-ts'; +import { description, id, meta, name, version } from '@kbn/securitysolution-io-ts-utils'; -import { _version, description, id, meta, name, version } from '../common/schemas'; +import { _version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const updateListSchema = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts index 889231d3aa640..54b312fcfdb37 100644 --- a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getAcknowledgeSchemaResponseMock } from './acknowledge_schema.mock'; import { AcknowledgeSchema, acknowledgeSchema } from './acknowledge_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts index 624687d4fc427..1f38044409b2c 100644 --- a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { CreateEndpointListSchema, createEndpointListSchema } from './create_endpoint_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts index ab2aac39c19d2..c7d1459319eaf 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -48,10 +48,6 @@ export const getExceptionListItemSchemaMock = ( ...(overrides || {}), }); -export const getExceptionListItemSchemaXMock = (count = 1): ExceptionListItemSchema[] => { - return new Array(count).fill(null).map(() => getExceptionListItemSchemaMock()); -}; - /** * This is useful for end to end tests where we remove the auto generated parts for comparisons * such as created_at, updated_at, and id. diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts index 50a12008e6579..b4809ee17b4bb 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; import { ExceptionListItemSchema, exceptionListItemSchema } from './exception_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts index 0efda4316104d..769cfb3548ced 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts @@ -6,26 +6,29 @@ */ import * as t from 'io-ts'; - import { - _versionOrUndefined, + commentsArray, created_at, created_by, description, + entriesArray, exceptionListItemType, id, - item_id, - list_id, metaOrUndefined, name, - namespace_type, osTypeArray, tags, - tie_breaker_id, updated_at, updated_by, +} from '@kbn/securitysolution-io-ts-utils'; + +import { + _versionOrUndefined, + item_id, + list_id, + namespace_type, + tie_breaker_id, } from '../common/schemas'; -import { commentsArray, entriesArray } from '../types'; export const exceptionListItemSchema = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts index b0e8711c7acb6..ef2b639ba2f06 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { ExceptionListSchema, exceptionListSchema } from './exception_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts index 578ced43db519..880c2d4f89e4f 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts @@ -6,24 +6,26 @@ */ import * as t from 'io-ts'; - import { - _versionOrUndefined, created_at, created_by, description, exceptionListType, id, - immutable, - list_id, metaOrUndefined, name, - namespace_type, osTypeArray, tags, - tie_breaker_id, updated_at, updated_by, +} from '@kbn/securitysolution-io-ts-utils'; + +import { + _versionOrUndefined, + immutable, + list_id, + namespace_type, + tie_breaker_id, version, } from '../common/schemas'; diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts index e3e148b344dae..b04d9fbdad3b7 100644 --- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; import { getFoundExceptionListItemSchemaMock } from './found_exception_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts index 1b8a600538cbd..cebf8ccc5d0d3 100644 --- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { getFoundExceptionListSchemaMock } from './found_exception_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts index 8abd0a3c7d8b5..4a0592d49228e 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getListItemIndexExistSchemaResponseMock } from './list_item_index_exist_schema.mock'; import { ListItemIndexExistSchema, listItemIndexExistSchema } from './list_item_index_exist_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts index 8cc82b43f1f8d..ffe49f305d484 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getListItemResponseMock } from './list_item_schema.mock'; import { ListItemSchema, listItemSchema } from './list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts index beedd50342018..1f105afac8b44 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts @@ -6,20 +6,22 @@ */ import * as t from 'io-ts'; - import { - _versionOrUndefined, created_at, created_by, - deserializerOrUndefined, id, - list_id, metaOrUndefined, - serializerOrUndefined, - tie_breaker_id, type, updated_at, updated_by, +} from '@kbn/securitysolution-io-ts-utils'; + +import { + _versionOrUndefined, + deserializerOrUndefined, + list_id, + serializerOrUndefined, + tie_breaker_id, value, } from '../common/schemas'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts index bca42dd948bd3..294f7da9a098f 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getListResponseMock } from './list_schema.mock'; import { ListSchema, listSchema } from './list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.ts index 87d13a2071790..58abe94772ff6 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.ts @@ -6,22 +6,24 @@ */ import * as t from 'io-ts'; - import { - _versionOrUndefined, created_at, created_by, description, - deserializerOrUndefined, id, - immutable, metaOrUndefined, name, - serializerOrUndefined, - tie_breaker_id, type, updated_at, updated_by, +} from '@kbn/securitysolution-io-ts-utils'; + +import { + _versionOrUndefined, + deserializerOrUndefined, + immutable, + serializerOrUndefined, + tie_breaker_id, version, } from '../common/schemas'; diff --git a/x-pack/plugins/lists/common/schemas/response/search_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/search_list_item_schema.test.ts index 2bebd7bafcd6c..4919e6a5ca73c 100644 --- a/x-pack/plugins/lists/common/schemas/response/search_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/search_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getSearchListItemResponseMock } from './search_list_item_schema.mock'; import { SearchListItemSchema, searchListItemSchema } from './search_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/types/comment.mock.ts b/x-pack/plugins/lists/common/schemas/types/comment.mock.ts index cad038344d941..75b4b6a431ac3 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.mock.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { DATE_NOW, ID, USER } from '../../constants.mock'; +import { Comment, CommentsArray } from '@kbn/securitysolution-io-ts-utils'; -import { Comment, CommentsArray } from './comment'; +import { DATE_NOW, ID, USER } from '../../constants.mock'; export const getCommentsMock = (): Comment => ({ comment: 'some old comment', diff --git a/x-pack/plugins/lists/common/schemas/types/comment.test.ts b/x-pack/plugins/lists/common/schemas/types/comment.test.ts deleted file mode 100644 index 3625d1ad7f1af..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/comment.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { DATE_NOW } from '../../constants.mock'; -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { getCommentsArrayMock, getCommentsMock } from './comment.mock'; -import { - Comment, - CommentsArray, - CommentsArrayOrUndefined, - comment, - commentsArray, - commentsArrayOrUndefined, -} from './comment'; - -describe('Comment', () => { - describe('comment', () => { - test('it fails validation when "id" is undefined', () => { - const payload = { ...getCommentsMock(), id: undefined }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it passes validation with a typical comment', () => { - const payload = getCommentsMock(); - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it passes validation with "updated_at" and "updated_by" fields included', () => { - const payload = getCommentsMock(); - payload.updated_at = DATE_NOW; - payload.updated_by = 'someone'; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it fails validation when undefined', () => { - const payload = undefined; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "comment" is an empty string', () => { - const payload: Omit & { comment: string } = { - ...getCommentsMock(), - comment: '', - }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "comment"']); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "comment" is not a string', () => { - const payload: Omit & { comment: string[] } = { - ...getCommentsMock(), - comment: ['some value'], - }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "["some value"]" supplied to "comment"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "created_at" is not a string', () => { - const payload: Omit & { created_at: number } = { - ...getCommentsMock(), - created_at: 1, - }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "created_at"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "created_by" is not a string', () => { - const payload: Omit & { created_by: number } = { - ...getCommentsMock(), - created_by: 1, - }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "created_by"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "updated_at" is not a string', () => { - const payload: Omit & { updated_at: number } = { - ...getCommentsMock(), - updated_at: 1, - }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "updated_at"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "updated_by" is not a string', () => { - const payload: Omit & { updated_by: number } = { - ...getCommentsMock(), - updated_by: 1, - }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "updated_by"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: Comment & { - extraKey?: string; - } = getCommentsMock(); - payload.extraKey = 'some value'; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getCommentsMock()); - }); - }); - - describe('commentsArray', () => { - test('it passes validation an array of Comment', () => { - const payload = getCommentsArrayMock(); - const decoded = commentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it passes validation when a Comment includes "updated_at" and "updated_by"', () => { - const commentsPayload = getCommentsMock(); - commentsPayload.updated_at = DATE_NOW; - commentsPayload.updated_by = 'someone'; - const payload = [{ ...commentsPayload }, ...getCommentsArrayMock()]; - const decoded = commentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it fails validation when undefined', () => { - const payload = undefined; - const decoded = commentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when array includes non Comment types', () => { - const payload = ([1] as unknown) as CommentsArray; - const decoded = commentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('commentsArrayOrUndefined', () => { - test('it passes validation an array of Comment', () => { - const payload = getCommentsArrayMock(); - const decoded = commentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it passes validation when undefined', () => { - const payload = undefined; - const decoded = commentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it fails validation when array includes non Comment types', () => { - const payload = ([1] as unknown) as CommentsArrayOrUndefined; - const decoded = commentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/comment.ts b/x-pack/plugins/lists/common/schemas/types/comment.ts deleted file mode 100644 index 016ef1b75edf8..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/comment.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../shared_imports'; -import { created_at, created_by, id, updated_at, updated_by } from '../common/schemas'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const comment = t.intersection([ - t.exact( - t.type({ - comment: NonEmptyString, - created_at, - created_by, - id, - }) - ), - t.exact( - t.partial({ - updated_at, - updated_by, - }) - ), -]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const commentsArray = t.array(comment); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type CommentsArray = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Comment = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type CommentsArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts index 78d54d1d01616..2d8dd7b462258 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CreateComment, CreateCommentsArray } from './create_comment'; +import { CreateComment, CreateCommentsArray } from '@kbn/securitysolution-io-ts-utils'; export const getCreateCommentsMock = (): CreateComment => ({ comment: 'some comments', diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts deleted file mode 100644 index aed3860c0e2ef..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { getCreateCommentsArrayMock, getCreateCommentsMock } from './create_comment.mock'; -import { - CreateComment, - CreateCommentsArray, - CreateCommentsArrayOrUndefined, - createComment, - createCommentsArray, - createCommentsArrayOrUndefined, -} from './create_comment'; - -describe('CreateComment', () => { - describe('createComment', () => { - test('it passes validation with a default comment', () => { - const payload = getCreateCommentsMock(); - const decoded = createComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it fails validation when undefined', () => { - const payload = undefined; - const decoded = createComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "{| comment: NonEmptyString |}"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "comment" is not a string', () => { - const payload: Omit & { comment: string[] } = { - ...getCreateCommentsMock(), - comment: ['some value'], - }; - const decoded = createComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "["some value"]" supplied to "comment"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: CreateComment & { - extraKey?: string; - } = getCreateCommentsMock(); - payload.extraKey = 'some value'; - const decoded = createComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getCreateCommentsMock()); - }); - }); - - describe('createCommentsArray', () => { - test('it passes validation an array of comments', () => { - const payload = getCreateCommentsArrayMock(); - const decoded = createCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it fails validation when undefined', () => { - const payload = undefined; - const decoded = createCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "Array<{| comment: NonEmptyString |}>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when array includes non comments types', () => { - const payload = ([1] as unknown) as CreateCommentsArray; - const decoded = createCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('createCommentsArrayOrUndefined', () => { - test('it passes validation an array of comments', () => { - const payload = getCreateCommentsArrayMock(); - const decoded = createCommentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it passes validation when undefined', () => { - const payload = undefined; - const decoded = createCommentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it fails validation when array includes non comments types', () => { - const payload = ([1] as unknown) as CreateCommentsArrayOrUndefined; - const decoded = createCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"', - ]); - expect(message.schema).toEqual({}); - }); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.ts deleted file mode 100644 index 070e860299f3d..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../shared_imports'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const createComment = t.exact( - t.type({ - comment: NonEmptyString, - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type CreateComment = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const createCommentsArray = t.array(createComment); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type CreateCommentsArray = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type CreateComments = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const createCommentsArrayOrUndefined = t.union([createCommentsArray, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type CreateCommentsArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts deleted file mode 100644 index 0ff1dad5cf515..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { DefaultCommentsArray } from './default_comments_array'; -import { CommentsArray } from './comment'; -import { getCommentsArrayMock } from './comment.mock'; - -describe('default_comments_array', () => { - test('it should pass validation when supplied an empty array', () => { - const payload: CommentsArray = []; - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should pass validation when supplied an array of comments', () => { - const payload: CommentsArray = getCommentsArrayMock(); - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation when supplied an array of numbers', () => { - const payload = [1]; - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when supplied an array of strings', () => { - const payload = ['some string']; - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts deleted file mode 100644 index b190bfb649a9f..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -import { CommentsArray, comment } from './comment'; - -/** - * Types the DefaultCommentsArray as: - * - If null or undefined, then a default array of type entry will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultCommentsArray = new t.Type( - 'DefaultCommentsArray', - t.array(comment).is, - (input): Either => - input == null ? t.success([]) : t.array(comment).decode(input), - t.identity -); diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts deleted file mode 100644 index 089fb0a68f050..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { DefaultCreateCommentsArray } from './default_create_comments_array'; -import { CreateCommentsArray } from './create_comment'; -import { getCreateCommentsArrayMock } from './create_comment.mock'; -import { getCommentsArrayMock } from './comment.mock'; - -describe('default_create_comments_array', () => { - test('it should pass validation when an empty array', () => { - const payload: CreateCommentsArray = []; - const decoded = DefaultCreateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should pass validation when an array of comments', () => { - const payload: CreateCommentsArray = getCreateCommentsArrayMock(); - const decoded = DefaultCreateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should strip out "created_at" and "created_by" if they are passed in', () => { - const payload = getCommentsArrayMock(); - const decoded = DefaultCreateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - // TODO: Known weird error formatting that is on our list to address - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([ - { comment: 'some old comment' }, - { comment: 'some old comment' }, - ]); - }); - - test('it should not pass validation when an array of numbers', () => { - const payload = [1]; - const decoded = DefaultCreateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - // TODO: Known weird error formatting that is on our list to address - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not pass validation when an array of strings', () => { - const payload = ['some string']; - const decoded = DefaultCreateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "Array<{| comment: NonEmptyString |}>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = DefaultCreateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts deleted file mode 100644 index 92121aaf05a6b..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -import { CreateCommentsArray, createComment } from './create_comment'; - -/** - * Types the DefaultCreateComments as: - * - If null or undefined, then a default array of type entry will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultCreateCommentsArray = new t.Type< - CreateCommentsArray, - CreateCommentsArray, - unknown ->( - 'DefaultCreateComments', - t.array(createComment).is, - (input): Either => - input == null ? t.success([]) : t.array(createComment).decode(input), - t.identity -); diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts deleted file mode 100644 index edb7f06d4505b..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { DefaultNamespace } from './default_namespace'; - -describe('default_namespace', () => { - test('it should validate "single"', () => { - const payload = 'single'; - const decoded = DefaultNamespace.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate "agnostic"', () => { - const payload = 'agnostic'; - const decoded = DefaultNamespace.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it defaults to "single" if "undefined"', () => { - const payload = undefined; - const decoded = DefaultNamespace.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('single'); - }); - - test('it defaults to "single" if "null"', () => { - const payload = null; - const decoded = DefaultNamespace.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('single'); - }); - - test('it should FAIL validation if not "single" or "agnostic"', () => { - const payload = 'something else'; - const decoded = DefaultNamespace.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - `Invalid value "something else" supplied to "DefaultNamespace"`, - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts deleted file mode 100644 index c5ae93b0a11a5..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -export const namespaceType = t.keyof({ agnostic: null, single: null }); -export type NamespaceType = t.TypeOf; - -/** - * Types the DefaultNamespace as: - * - If null or undefined, then a default string/enumeration of "single" will be used. - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultNamespace = new t.Type( - 'DefaultNamespace', - namespaceType.is, - (input, context): Either => - input == null ? t.success('single') : namespaceType.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts deleted file mode 100644 index d793296f88d53..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { DefaultNamespaceArray, DefaultNamespaceArrayType } from './default_namespace_array'; - -describe('default_namespace_array', () => { - test('it should validate "null" single item as an array with a "single" value', () => { - const payload: DefaultNamespaceArrayType = null; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['single']); - }); - - test('it should FAIL validation of numeric value', () => { - const payload = 5; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultNamespaceArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate "undefined" item as an array with a "single" value', () => { - const payload: DefaultNamespaceArrayType = undefined; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['single']); - }); - - test('it should validate "single" as an array of a "single" value', () => { - const payload: DefaultNamespaceArrayType = 'single'; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([payload]); - }); - - test('it should validate "agnostic" as an array of a "agnostic" value', () => { - const payload: DefaultNamespaceArrayType = 'agnostic'; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([payload]); - }); - - test('it should validate "single,agnostic" as an array of 2 values of ["single", "agnostic"] values', () => { - const payload: DefaultNamespaceArrayType = 'agnostic,single'; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['agnostic', 'single']); - }); - - test('it should validate 3 elements of "single,agnostic,single" as an array of 3 values of ["single", "agnostic", "single"] values', () => { - const payload: DefaultNamespaceArrayType = 'single,agnostic,single'; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['single', 'agnostic', 'single']); - }); - - test('it should validate 3 elements of "single,agnostic, single" as an array of 3 values of ["single", "agnostic", "single"] values when there are spaces', () => { - const payload: DefaultNamespaceArrayType = ' single, agnostic, single '; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['single', 'agnostic', 'single']); - }); - - test('it should FAIL validation when given 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { - const payload: DefaultNamespaceArrayType = 'single,agnostic,junk'; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "junk" supplied to "DefaultNamespaceArray"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts deleted file mode 100644 index c27e4eade4b38..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -import { namespaceType } from './default_namespace'; - -export const namespaceTypeArray = t.array(namespaceType); -export type NamespaceTypeArray = t.TypeOf; - -/** - * Types the DefaultNamespaceArray as: - * - If null or undefined, then a default string array of "single" will be used. - * - If it contains a string, then it is split along the commas and puts them into an array and validates it - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultNamespaceArray = new t.Type< - NamespaceTypeArray, - string | undefined | null, - unknown ->( - 'DefaultNamespaceArray', - namespaceTypeArray.is, - (input, context): Either => { - if (input == null) { - return t.success(['single']); - } else if (typeof input === 'string') { - const commaSeparatedValues = input - .trim() - .split(',') - .map((value) => value.trim()); - return namespaceTypeArray.validate(commaSeparatedValues, context); - } - return t.failure(input, context); - }, - String -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type DefaultNamespaceArrayType = t.OutputOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type DefaultNamespaceArrayTypeDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/default_string_boolean_false.test.ts b/x-pack/plugins/lists/common/schemas/types/default_string_boolean_false.test.ts deleted file mode 100644 index eccbb8bf9506a..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/default_string_boolean_false.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../test_utils'; - -import { DefaultStringBooleanFalse } from './default_string_boolean_false'; - -describe('default_string_boolean_false', () => { - test('it should validate a boolean false', () => { - const payload = false; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a boolean true', () => { - const payload = true; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultStringBooleanFalse"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default false', () => { - const payload = null; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(false); - }); - - test('it should return a default false when given a string of "false"', () => { - const payload = 'false'; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(false); - }); - - test('it should return a default true when given a string of "true"', () => { - const payload = 'true'; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(true); - }); - - test('it should return a default true when given a string of "TruE"', () => { - const payload = 'TruE'; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(true); - }); - - test('it should not work with a string of junk "junk"', () => { - const payload = 'junk'; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "junk" supplied to "DefaultStringBooleanFalse"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not work with an empty string', () => { - const payload = ''; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "" supplied to "DefaultStringBooleanFalse"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_string_boolean_false.ts b/x-pack/plugins/lists/common/schemas/types/default_string_boolean_false.ts deleted file mode 100644 index d409648b5435b..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/default_string_boolean_false.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the DefaultStringBooleanFalse as: - * - If a string this will convert the string to a boolean - * - If null or undefined, then a default false will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultStringBooleanFalse = new t.Type( - 'DefaultStringBooleanFalse', - t.boolean.is, - (input, context): Either => { - if (input == null) { - return t.success(false); - } else if (typeof input === 'string' && input.toLowerCase() === 'true') { - return t.success(true); - } else if (typeof input === 'string' && input.toLowerCase() === 'false') { - return t.success(false); - } else { - return t.boolean.validate(input, context); - } - }, - t.identity -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type DefaultStringBooleanFalseC = typeof DefaultStringBooleanFalse; diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts deleted file mode 100644 index 6f7fa678f0f20..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { DefaultUpdateCommentsArray } from './default_update_comments_array'; -import { UpdateCommentsArray } from './update_comment'; -import { getUpdateCommentsArrayMock } from './update_comment.mock'; - -describe('default_update_comments_array', () => { - test('it should pass validation when supplied an empty array', () => { - const payload: UpdateCommentsArray = []; - const decoded = DefaultUpdateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should pass validation when supplied an array of comments', () => { - const payload: UpdateCommentsArray = getUpdateCommentsArrayMock(); - const decoded = DefaultUpdateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation when supplied an array of numbers', () => { - const payload = [1]; - const decoded = DefaultUpdateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when supplied an array of strings', () => { - const payload = ['some string']; - const decoded = DefaultUpdateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = DefaultUpdateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts deleted file mode 100644 index 43749b606e4ee..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -import { UpdateCommentsArray, updateCommentsArray } from './update_comment'; - -/** - * Types the DefaultCommentsUpdate as: - * - If null or undefined, then a default array of type entry will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultUpdateCommentsArray = new t.Type< - UpdateCommentsArray, - UpdateCommentsArray, - unknown ->( - 'DefaultCreateComments', - updateCommentsArray.is, - (input): Either => - input == null ? t.success([]) : updateCommentsArray.decode(input), - t.identity -); diff --git a/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts deleted file mode 100644 index 7921416c35a58..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { EmptyStringArray, EmptyStringArrayEncoded } from './empty_string_array'; - -describe('empty_string_array', () => { - test('it should validate "null" and create an empty array', () => { - const payload: EmptyStringArrayEncoded = null; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); - - test('it should validate "undefined" and create an empty array', () => { - const payload: EmptyStringArrayEncoded = undefined; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); - - test('it should validate a single value of "a" into an array of size 1 of ["a"]', () => { - const payload: EmptyStringArrayEncoded = 'a'; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['a']); - }); - - test('it should validate 2 values of "a,b" into an array of size 2 of ["a", "b"]', () => { - const payload: EmptyStringArrayEncoded = 'a,b'; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['a', 'b']); - }); - - test('it should validate 3 values of "a,b,c" into an array of size 3 of ["a", "b", "c"]', () => { - const payload: EmptyStringArrayEncoded = 'a,b,c'; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['a', 'b', 'c']); - }); - - test('it should FAIL validation of number', () => { - const payload: number = 5; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "EmptyStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate 3 values of " a, b, c " into an array of size 3 of ["a", "b", "c"] even though they have spaces', () => { - const payload: EmptyStringArrayEncoded = ' a, b, c '; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['a', 'b', 'c']); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/empty_string_array.ts b/x-pack/plugins/lists/common/schemas/types/empty_string_array.ts deleted file mode 100644 index 6192417600870..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/empty_string_array.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the EmptyStringArray as: - * - A value that can be undefined, or null (which will be turned into an empty array) - * - A comma separated string that can turn into an array by splitting on it - * - Example input converted to output: undefined -> [] - * - Example input converted to output: null -> [] - * - Example input converted to output: "a,b,c" -> ["a", "b", "c"] - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const EmptyStringArray = new t.Type( - 'EmptyStringArray', - t.array(t.string).is, - (input, context): Either => { - if (input == null) { - return t.success([]); - } else if (typeof input === 'string' && input.trim() !== '') { - const arrayValues = input - .trim() - .split(',') - .map((value) => value.trim()); - const emptyValueFound = arrayValues.some((value) => value === ''); - if (emptyValueFound) { - return t.failure(input, context); - } else { - return t.success(arrayValues); - } - } else { - return t.failure(input, context); - } - }, - String -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EmptyStringArrayEncoded = t.OutputOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EmptyStringArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entries.mock.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entries.mock.ts deleted file mode 100644 index 8e7ede596871a..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entries.mock.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EndpointEntriesArray } from './entries'; -import { getEndpointEntryMatchMock } from './entry_match.mock'; -import { getEndpointEntryMatchAnyMock } from './entry_match_any.mock'; -import { getEndpointEntryNestedMock } from './entry_nested.mock'; - -export const getEndpointEntriesArrayMock = (): EndpointEntriesArray => [ - getEndpointEntryMatchMock(), - getEndpointEntryMatchAnyMock(), - getEndpointEntryNestedMock(), -]; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entries.test.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entries.test.ts deleted file mode 100644 index 4835b607d681f..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entries.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../../shared_imports'; -import { getEntryExistsMock } from '../entry_exists.mock'; -import { getEntryListMock } from '../entry_list.mock'; - -import { getEndpointEntryMatchMock } from './entry_match.mock'; -import { getEndpointEntryMatchAnyMock } from './entry_match_any.mock'; -import { getEndpointEntryNestedMock } from './entry_nested.mock'; -import { getEndpointEntriesArrayMock } from './entries.mock'; -import { - NonEmptyEndpointEntriesArray, - endpointEntriesArray, - nonEmptyEndpointEntriesArray, -} from './entries'; - -describe('Endpoint', () => { - describe('entriesArray', () => { - test('it should validate an array with match entry', () => { - const payload = [getEndpointEntryMatchMock()]; - const decoded = endpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with match_any entry', () => { - const payload = [getEndpointEntryMatchAnyMock()]; - const decoded = endpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate an empty array', () => { - const payload: NonEmptyEndpointEntriesArray = []; - const decoded = nonEmptyEndpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[]" supplied to "NonEmptyEndpointEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('type guard for nonEmptyEndpointNestedEntries should allow array of endpoint entries', () => { - const payload: NonEmptyEndpointEntriesArray = [getEndpointEntryMatchAnyMock()]; - const guarded = nonEmptyEndpointEntriesArray.is(payload); - expect(guarded).toBeTruthy(); - }); - - test('type guard for nonEmptyEndpointNestedEntries should disallow empty arrays', () => { - const payload: NonEmptyEndpointEntriesArray = []; - const guarded = nonEmptyEndpointEntriesArray.is(payload); - expect(guarded).toBeFalsy(); - }); - - test('it should NOT validate an array with exists entry', () => { - const payload = [getEntryExistsMock()]; - const decoded = endpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "exists" supplied to "type"', - 'Invalid value "undefined" supplied to "value"', - 'Invalid value "undefined" supplied to "entries"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate an array with list entry', () => { - const payload = [getEntryListMock()]; - const decoded = endpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "list" supplied to "type"', - 'Invalid value "undefined" supplied to "value"', - 'Invalid value "undefined" supplied to "entries"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate an array with nested entry', () => { - const payload = [getEndpointEntryNestedMock()]; - const decoded = endpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with all types of entries', () => { - const payload = getEndpointEntriesArrayMock(); - const decoded = endpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entries.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entries.ts deleted file mode 100644 index 4622a8a7d39b7..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entries.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -import { endpointEntryMatchAny } from './entry_match_any'; -import { endpointEntryMatch } from './entry_match'; -import { endpointEntryNested } from './entry_nested'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const endpointEntriesArray = t.array( - t.union([endpointEntryMatch, endpointEntryMatchAny, endpointEntryNested]) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EndpointEntriesArray = t.TypeOf; - -/** - * Types the nonEmptyEndpointEntriesArray as: - * - An array of entries of length 1 or greater - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const nonEmptyEndpointEntriesArray = new t.Type< - EndpointEntriesArray, - EndpointEntriesArray, - unknown ->( - 'NonEmptyEndpointEntriesArray', - (u: unknown): u is EndpointEntriesArray => endpointEntriesArray.is(u) && u.length > 0, - (input, context): Either => { - if (Array.isArray(input) && input.length === 0) { - return t.failure(input, context); - } else { - return endpointEntriesArray.validate(input, context); - } - }, - t.identity -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type NonEmptyEndpointEntriesArray = t.OutputOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type NonEmptyEndpointEntriesArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.mock.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.mock.ts deleted file mode 100644 index ebdee20a6ebc8..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.mock.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../../constants.mock'; - -import { EndpointEntryMatch } from './entry_match'; - -export const getEndpointEntryMatchMock = (): EndpointEntryMatch => ({ - field: FIELD, - operator: OPERATOR, - type: MATCH, - value: ENTRY_VALUE, -}); diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.test.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.test.ts deleted file mode 100644 index 7a970b873ba06..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../../shared_imports'; -import { getEntryMatchMock } from '../entry_match.mock'; - -import { getEndpointEntryMatchMock } from './entry_match.mock'; -import { EndpointEntryMatch, endpointEntryMatch } from './entry_match'; - -describe('endpointEntryMatch', () => { - test('it should validate an entry', () => { - const payload = getEndpointEntryMatchMock(); - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate when "operator" is "excluded"', () => { - // Use the generic entry mock so we can test operator: excluded - const payload = getEntryMatchMock(); - payload.operator = 'excluded'; - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "excluded" supplied to "operator"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "field" is empty string', () => { - const payload: Omit & { field: string } = { - ...getEndpointEntryMatchMock(), - field: '', - }; - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "value" is not string', () => { - const payload: Omit & { value: string[] } = { - ...getEndpointEntryMatchMock(), - value: ['some value'], - }; - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "["some value"]" supplied to "value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "value" is empty string', () => { - const payload: Omit & { value: string } = { - ...getEndpointEntryMatchMock(), - value: '', - }; - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "type" is not "match"', () => { - const payload: Omit & { type: string } = { - ...getEndpointEntryMatchMock(), - type: 'match_any', - }; - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "match_any" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EndpointEntryMatch & { - extraKey?: string; - } = getEndpointEntryMatchMock(); - payload.extraKey = 'some value'; - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryMatchMock()); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.ts deleted file mode 100644 index e4c24aa5e3560..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../../shared_imports'; -import { operatorIncluded } from '../../common/schemas'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const endpointEntryMatch = t.exact( - t.type({ - field: NonEmptyString, - operator: operatorIncluded, - type: t.keyof({ match: null }), - value: NonEmptyString, - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EndpointEntryMatch = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.mock.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.mock.ts deleted file mode 100644 index dd884056b5c7c..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.mock.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../../constants.mock'; - -import { EndpointEntryMatchAny } from './entry_match_any'; - -export const getEndpointEntryMatchAnyMock = (): EndpointEntryMatchAny => ({ - field: FIELD, - operator: OPERATOR, - type: MATCH_ANY, - value: [ENTRY_VALUE], -}); diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.test.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.test.ts deleted file mode 100644 index e460c8ceb86ff..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../../shared_imports'; -import { getEntryMatchAnyMock } from '../entry_match_any.mock'; - -import { getEndpointEntryMatchAnyMock } from './entry_match_any.mock'; -import { EndpointEntryMatchAny, endpointEntryMatchAny } from './entry_match_any'; - -describe('endpointEntryMatchAny', () => { - test('it should validate an entry', () => { - const payload = getEndpointEntryMatchAnyMock(); - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate when operator is "excluded"', () => { - // Use the generic entry mock so we can test operator: excluded - const payload = getEntryMatchAnyMock(); - payload.operator = 'excluded'; - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "excluded" supplied to "operator"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when field is empty string', () => { - const payload: Omit & { field: string } = { - ...getEndpointEntryMatchAnyMock(), - field: '', - }; - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when value is empty array', () => { - const payload: Omit & { value: string[] } = { - ...getEndpointEntryMatchAnyMock(), - value: [], - }; - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "[]" supplied to "value"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when value is not string array', () => { - const payload: Omit & { value: string } = { - ...getEndpointEntryMatchAnyMock(), - value: 'some string', - }; - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "type" is not "match_any"', () => { - const payload: Omit & { type: string } = { - ...getEndpointEntryMatchAnyMock(), - type: 'match', - }; - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EndpointEntryMatchAny & { - extraKey?: string; - } = getEndpointEntryMatchAnyMock(); - payload.extraKey = 'some extra key'; - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryMatchAnyMock()); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.ts deleted file mode 100644 index ffafd0f786547..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../../shared_imports'; -import { operatorIncluded } from '../../common/schemas'; -import { nonEmptyOrNullableStringArray } from '../non_empty_or_nullable_string_array'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const endpointEntryMatchAny = t.exact( - t.type({ - field: NonEmptyString, - operator: operatorIncluded, - type: t.keyof({ match_any: null }), - value: nonEmptyOrNullableStringArray, - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EndpointEntryMatchAny = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_wildcard.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_wildcard.ts deleted file mode 100644 index ca4894991664c..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_wildcard.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../../shared_imports'; -import { operatorIncluded } from '../../common/schemas'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const endpointEntryMatchWildcard = t.exact( - t.type({ - field: NonEmptyString, - operator: operatorIncluded, - type: t.keyof({ wildcard: null }), - value: NonEmptyString, - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EndpointEntryMatchWildcard = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.mock.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.mock.ts deleted file mode 100644 index 6a6aeb7a229df..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.mock.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FIELD, NESTED } from '../../../constants.mock'; - -import { EndpointEntryNested } from './entry_nested'; -import { getEndpointEntryMatchMock } from './entry_match.mock'; -import { getEndpointEntryMatchAnyMock } from './entry_match_any.mock'; - -export const getEndpointEntryNestedMock = (): EndpointEntryNested => ({ - entries: [getEndpointEntryMatchMock(), getEndpointEntryMatchAnyMock()], - field: FIELD, - type: NESTED, -}); diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.test.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.test.ts deleted file mode 100644 index 75432db0da4e4..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../../shared_imports'; -import { getEntryExistsMock } from '../entry_exists.mock'; - -import { getEndpointEntryNestedMock } from './entry_nested.mock'; -import { EndpointEntryNested, endpointEntryNested } from './entry_nested'; -import { getEndpointEntryMatchAnyMock } from './entry_match_any.mock'; -import { - NonEmptyEndpointNestedEntriesArray, - nonEmptyEndpointNestedEntriesArray, -} from './non_empty_nested_entries_array'; -import { getEndpointEntryMatchMock } from './entry_match.mock'; - -describe('endpointEntryNested', () => { - test('it should validate a nested entry', () => { - const payload = getEndpointEntryNestedMock(); - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when "type" is not "nested"', () => { - const payload: Omit & { type: 'match' } = { - ...getEndpointEntryNestedMock(), - type: 'match', - }; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "field" is empty string', () => { - const payload: Omit & { - field: string; - } = { ...getEndpointEntryNestedMock(), field: '' }; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "field" is not a string', () => { - const payload: Omit & { - field: number; - } = { ...getEndpointEntryNestedMock(), field: 1 }; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "entries" is not an array', () => { - const payload: Omit & { - entries: string; - } = { ...getEndpointEntryNestedMock(), entries: 'im a string' }; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "im a string" supplied to "entries"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate when "entries" contains an entry item that is type "match"', () => { - const payload = { ...getEndpointEntryNestedMock(), entries: [getEndpointEntryMatchAnyMock()] }; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ - entries: [ - { - field: 'host.name', - operator: 'included', - type: 'match_any', - value: ['some host name'], - }, - ], - field: 'host.name', - type: 'nested', - }); - }); - - test('it should NOT validate when "entries" contains an entry item that is type "exists"', () => { - const payload = { ...getEndpointEntryNestedMock(), entries: [getEntryExistsMock()] }; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "exists" supplied to "entries,type"', - 'Invalid value "undefined" supplied to "entries,value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EndpointEntryNested & { - extraKey?: string; - } = getEndpointEntryNestedMock(); - payload.extraKey = 'some extra key'; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEndpointEntryNestedMock()); - }); - - test('type guard for nonEmptyEndpointNestedEntries should allow array of endpoint entries', () => { - const payload: NonEmptyEndpointNestedEntriesArray = [ - getEndpointEntryMatchMock(), - getEndpointEntryMatchAnyMock(), - ]; - const guarded = nonEmptyEndpointNestedEntriesArray.is(payload); - expect(guarded).toBeTruthy(); - }); - - test('type guard for nonEmptyEndpointNestedEntries should disallow empty arrays', () => { - const payload: NonEmptyEndpointNestedEntriesArray = []; - const guarded = nonEmptyEndpointNestedEntriesArray.is(payload); - expect(guarded).toBeFalsy(); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.ts deleted file mode 100644 index 4304b7cd06c37..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../../shared_imports'; - -import { nonEmptyEndpointNestedEntriesArray } from './non_empty_nested_entries_array'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const endpointEntryNested = t.exact( - t.type({ - entries: nonEmptyEndpointNestedEntriesArray, - field: NonEmptyString, - type: t.keyof({ nested: null }), - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EndpointEntryNested = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/index.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/index.ts deleted file mode 100644 index fc8b8ec40882a..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './entries'; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/non_empty_nested_entries_array.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/non_empty_nested_entries_array.ts deleted file mode 100644 index e6bd3d61f7d78..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/non_empty_nested_entries_array.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -import { endpointEntryMatchAny } from './entry_match_any'; -import { endpointEntryMatch } from './entry_match'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const endpointNestedEntriesArray = t.array( - t.union([endpointEntryMatch, endpointEntryMatchAny]) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EndpointNestedEntriesArray = t.TypeOf; - -/** - * Types the nonEmptyNestedEntriesArray as: - * - An array of entries of length 1 or greater - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const nonEmptyEndpointNestedEntriesArray = new t.Type< - EndpointNestedEntriesArray, - EndpointNestedEntriesArray, - unknown ->( - 'NonEmptyEndpointNestedEntriesArray', - (u: unknown): u is EndpointNestedEntriesArray => endpointNestedEntriesArray.is(u) && u.length > 0, - (input, context): Either => { - if (Array.isArray(input) && input.length === 0) { - return t.failure(input, context); - } else { - return endpointNestedEntriesArray.validate(input, context); - } - }, - t.identity -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type NonEmptyEndpointNestedEntriesArray = t.OutputOf< - typeof nonEmptyEndpointNestedEntriesArray ->; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type NonEmptyEndpointNestedEntriesArrayDecoded = t.TypeOf< - typeof nonEmptyEndpointNestedEntriesArray ->; diff --git a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts index 319eb687c4ced..ee43a0b26ad54 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { EntriesArray } from './entries'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; + import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; import { getEntryListMock } from './entry_list.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entries.test.ts b/x-pack/plugins/lists/common/schemas/types/entries.test.ts deleted file mode 100644 index 2e1dd7a3da7cc..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entries.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { getEntryMatchMock } from './entry_match.mock'; -import { getEntryMatchAnyMock } from './entry_match_any.mock'; -import { getEntryListMock } from './entry_list.mock'; -import { getEntryExistsMock } from './entry_exists.mock'; -import { getEntryNestedMock } from './entry_nested.mock'; -import { getEntriesArrayMock } from './entries.mock'; -import { entriesArray, entriesArrayOrUndefined, entry } from './entries'; - -describe('Entries', () => { - describe('entry', () => { - test('it should validate a match entry', () => { - const payload = getEntryMatchMock(); - const decoded = entry.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a match_any entry', () => { - const payload = getEntryMatchAnyMock(); - const decoded = entry.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a exists entry', () => { - const payload = getEntryExistsMock(); - const decoded = entry.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a list entry', () => { - const payload = getEntryListMock(); - const decoded = entry.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation of nested entry', () => { - const payload = getEntryNestedMock(); - const decoded = entry.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "operator"', - 'Invalid value "nested" supplied to "type"', - 'Invalid value "undefined" supplied to "value"', - 'Invalid value "undefined" supplied to "list"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('entriesArray', () => { - test('it should validate an array with match entry', () => { - const payload = [getEntryMatchMock()]; - const decoded = entriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with match_any entry', () => { - const payload = [getEntryMatchAnyMock()]; - const decoded = entriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with exists entry', () => { - const payload = [getEntryExistsMock()]; - const decoded = entriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with list entry', () => { - const payload = [getEntryListMock()]; - const decoded = entriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with nested entry', () => { - const payload = [getEntryNestedMock()]; - const decoded = entriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with all types of entries', () => { - const payload = [...getEntriesArrayMock()]; - const decoded = entriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - }); - - describe('entriesArrayOrUndefined', () => { - test('it should validate undefined', () => { - const payload = undefined; - const decoded = entriesArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with nested entry', () => { - const payload = [getEntryNestedMock()]; - const decoded = entriesArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/entries.ts b/x-pack/plugins/lists/common/schemas/types/entries.ts deleted file mode 100644 index 043348070031b..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entries.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { entriesMatchAny } from './entry_match_any'; -import { entriesMatch } from './entry_match'; -import { entriesExists } from './entry_exists'; -import { entriesList } from './entry_list'; -import { entriesNested } from './entry_nested'; -import { entriesMatchWildcard } from './entry_match_wildcard'; - -// NOTE: Type nested is not included here to denote it's non-recursive nature. -// So a nested entry is really just a collection of `Entry` types. - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const entry = t.union([ - entriesMatch, - entriesMatchAny, - entriesList, - entriesExists, - entriesMatchWildcard, -]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Entry = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const entriesArray = t.array( - t.union([ - entriesMatch, - entriesMatchAny, - entriesList, - entriesExists, - entriesNested, - entriesMatchWildcard, - ]) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EntriesArray = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const entriesArrayOrUndefined = t.union([entriesArray, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EntriesArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts index 9c8f199d3a3a1..3e26d261f44ca 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { EXISTS, FIELD, OPERATOR } from '../../constants.mock'; +import { EntryExists } from '@kbn/securitysolution-io-ts-utils'; -import { EntryExists } from './entry_exists'; +import { EXISTS, FIELD, OPERATOR } from '../../constants.mock'; export const getEntryExistsMock = (): EntryExists => ({ field: FIELD, diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts deleted file mode 100644 index dafae6a45995f..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { getEntryExistsMock } from './entry_exists.mock'; -import { EntryExists, entriesExists } from './entry_exists'; - -describe('entriesExists', () => { - test('it should validate an entry', () => { - const payload = getEntryExistsMock(); - const decoded = entriesExists.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when "operator" is "included"', () => { - const payload = getEntryExistsMock(); - const decoded = entriesExists.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when "operator" is "excluded"', () => { - const payload = getEntryExistsMock(); - payload.operator = 'excluded'; - const decoded = entriesExists.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when "field" is empty string', () => { - const payload: Omit & { field: string } = { - ...getEntryExistsMock(), - field: '', - }; - const decoded = entriesExists.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EntryExists & { - extraKey?: string; - } = getEntryExistsMock(); - payload.extraKey = 'some extra key'; - const decoded = entriesExists.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryExistsMock()); - }); - - test('it should FAIL validation when "type" is not "exists"', () => { - const payload: Omit & { type: string } = { - ...getEntryExistsMock(), - type: 'match', - }; - const decoded = entriesExists.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.ts deleted file mode 100644 index 32fb8888c88dc..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../shared_imports'; -import { operator } from '../common/schemas'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const entriesExists = t.exact( - t.type({ - field: NonEmptyString, - operator, - type: t.keyof({ exists: null }), - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EntryExists = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts index d48d725a7c3f5..7eadfcdf3454c 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../../constants.mock'; +import { EntryList } from '@kbn/securitysolution-io-ts-utils'; -import { EntryList } from './entry_list'; +import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../../constants.mock'; export const getEntryListMock = (): EntryList => ({ field: FIELD, diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts deleted file mode 100644 index 4a4caf9e6bc4c..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { getEntryListMock } from './entry_list.mock'; -import { EntryList, entriesList } from './entry_list'; - -describe('entriesList', () => { - test('it should validate an entry', () => { - const payload = getEntryListMock(); - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when operator is "included"', () => { - const payload = getEntryListMock(); - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when "operator" is "excluded"', () => { - const payload = getEntryListMock(); - payload.operator = 'excluded'; - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when "list" is not expected value', () => { - const payload: Omit & { list: string } = { - ...getEntryListMock(), - list: 'someListId', - }; - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "someListId" supplied to "list"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "list.id" is empty string', () => { - const payload: Omit & { list: { id: string; type: 'ip' } } = { - ...getEntryListMock(), - list: { id: '', type: 'ip' }, - }; - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "list,id"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "type" is not "lists"', () => { - const payload: Omit & { type: 'match_any' } = { - ...getEntryListMock(), - type: 'match_any', - }; - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "match_any" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EntryList & { - extraKey?: string; - } = getEntryListMock(); - payload.extraKey = 'some extra key'; - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryListMock()); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.ts deleted file mode 100644 index 9b2b345244805..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../shared_imports'; -import { operator, type } from '../common/schemas'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const entriesList = t.exact( - t.type({ - field: NonEmptyString, - list: t.exact(t.type({ id: NonEmptyString, type })), - operator, - type: t.keyof({ list: null }), - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EntryList = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts index 7362d30ad52f1..bc0eb3b5c4f85 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants.mock'; +import { EntryMatch } from '@kbn/securitysolution-io-ts-utils'; -import { EntryMatch } from './entry_match'; +import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants.mock'; export const getEntryMatchMock = (): EntryMatch => ({ field: FIELD, diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts deleted file mode 100644 index c2331ca2dbeb8..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { getEntryMatchMock } from './entry_match.mock'; -import { EntryMatch, entriesMatch } from './entry_match'; - -describe('entriesMatch', () => { - test('it should validate an entry', () => { - const payload = getEntryMatchMock(); - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when operator is "included"', () => { - const payload = getEntryMatchMock(); - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when "operator" is "excluded"', () => { - const payload = getEntryMatchMock(); - payload.operator = 'excluded'; - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when "field" is empty string', () => { - const payload: Omit & { field: string } = { - ...getEntryMatchMock(), - field: '', - }; - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "value" is not string', () => { - const payload: Omit & { value: string[] } = { - ...getEntryMatchMock(), - value: ['some value'], - }; - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "["some value"]" supplied to "value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "value" is empty string', () => { - const payload: Omit & { value: string } = { - ...getEntryMatchMock(), - value: '', - }; - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "type" is not "match"', () => { - const payload: Omit & { type: string } = { - ...getEntryMatchMock(), - type: 'match_any', - }; - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "match_any" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EntryMatch & { - extraKey?: string; - } = getEntryMatchMock(); - payload.extraKey = 'some value'; - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryMatchMock()); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.ts deleted file mode 100644 index e3529f2d043be..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../shared_imports'; -import { operator } from '../common/schemas'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const entriesMatch = t.exact( - t.type({ - field: NonEmptyString, - operator, - type: t.keyof({ match: null }), - value: NonEmptyString, - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EntryMatch = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts index 2b92454fd1012..74c3abbaa5881 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants.mock'; +import { EntryMatchAny } from '@kbn/securitysolution-io-ts-utils'; -import { EntryMatchAny } from './entry_match_any'; +import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants.mock'; export const getEntryMatchAnyMock = (): EntryMatchAny => ({ field: FIELD, diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts deleted file mode 100644 index 63e386290afb0..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { getEntryMatchAnyMock } from './entry_match_any.mock'; -import { EntryMatchAny, entriesMatchAny } from './entry_match_any'; - -describe('entriesMatchAny', () => { - test('it should validate an entry', () => { - const payload = getEntryMatchAnyMock(); - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when operator is "included"', () => { - const payload = getEntryMatchAnyMock(); - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when operator is "excluded"', () => { - const payload = getEntryMatchAnyMock(); - payload.operator = 'excluded'; - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when field is empty string', () => { - const payload: Omit & { field: string } = { - ...getEntryMatchAnyMock(), - field: '', - }; - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when value is empty array', () => { - const payload: Omit & { value: string[] } = { - ...getEntryMatchAnyMock(), - value: [], - }; - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "[]" supplied to "value"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when value is not string array', () => { - const payload: Omit & { value: string } = { - ...getEntryMatchAnyMock(), - value: 'some string', - }; - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "type" is not "match_any"', () => { - const payload: Omit & { type: string } = { - ...getEntryMatchAnyMock(), - type: 'match', - }; - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EntryMatchAny & { - extraKey?: string; - } = getEntryMatchAnyMock(); - payload.extraKey = 'some extra key'; - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryMatchAnyMock()); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts deleted file mode 100644 index d4846c7949ebb..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../shared_imports'; -import { operator } from '../common/schemas'; - -import { nonEmptyOrNullableStringArray } from './non_empty_or_nullable_string_array'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const entriesMatchAny = t.exact( - t.type({ - field: NonEmptyString, - operator, - type: t.keyof({ match_any: null }), - value: nonEmptyOrNullableStringArray, - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EntryMatchAny = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts index 3204bbe064496..320664bd2f833 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../../constants.mock'; +import { EntryMatchWildcard } from '@kbn/securitysolution-io-ts-utils'; -import { EntryMatchWildcard } from './entry_match_wildcard'; +import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../../constants.mock'; export const getEntryMatchWildcardMock = (): EntryMatchWildcard => ({ field: FIELD, diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.test.ts deleted file mode 100644 index 53cfc4fdff1f5..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { getEntryMatchWildcardMock } from './entry_match_wildcard.mock'; -import { EntryMatchWildcard, entriesMatchWildcard } from './entry_match_wildcard'; - -describe('entriesMatchWildcard', () => { - test('it should validate an entry', () => { - const payload = getEntryMatchWildcardMock(); - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when operator is "included"', () => { - const payload = getEntryMatchWildcardMock(); - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when "operator" is "excluded"', () => { - const payload = getEntryMatchWildcardMock(); - payload.operator = 'excluded'; - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when "field" is empty string', () => { - const payload: Omit & { field: string } = { - ...getEntryMatchWildcardMock(), - field: '', - }; - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "value" is not string', () => { - const payload: Omit & { value: string[] } = { - ...getEntryMatchWildcardMock(), - value: ['some value'], - }; - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "["some value"]" supplied to "value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "value" is empty string', () => { - const payload: Omit & { value: string } = { - ...getEntryMatchWildcardMock(), - value: '', - }; - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "type" is not "wildcard"', () => { - const payload: Omit & { type: string } = { - ...getEntryMatchWildcardMock(), - type: 'match', - }; - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EntryMatchWildcard & { - extraKey?: string; - } = getEntryMatchWildcardMock(); - payload.extraKey = 'some value'; - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryMatchWildcardMock()); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.ts deleted file mode 100644 index 1c8cb4dcdd2d8..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../shared_imports'; -import { operator } from '../common/schemas'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const entriesMatchWildcard = t.exact( - t.type({ - field: NonEmptyString, - operator, - type: t.keyof({ wildcard: null }), - value: NonEmptyString, - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EntryMatchWildcard = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts index 8966964a29223..f1d0a2bc76926 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts @@ -5,9 +5,10 @@ * 2.0. */ +import { EntryNested } from '@kbn/securitysolution-io-ts-utils'; + import { NESTED, NESTED_FIELD } from '../../constants.mock'; -import { EntryNested } from './entry_nested'; import { getEntryMatchExcludeMock, getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyExcludeMock, getEntryMatchAnyMock } from './entry_match_any.mock'; import { getEntryExistsMock } from './entry_exists.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts deleted file mode 100644 index a94b46c1d4a9e..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { getEntryNestedMock } from './entry_nested.mock'; -import { EntryNested, entriesNested } from './entry_nested'; -import { getEntryMatchAnyMock } from './entry_match_any.mock'; -import { getEntryExistsMock } from './entry_exists.mock'; - -describe('entriesNested', () => { - test('it should validate a nested entry', () => { - const payload = getEntryNestedMock(); - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when "type" is not "nested"', () => { - const payload: Omit & { type: 'match' } = { - ...getEntryNestedMock(), - type: 'match', - }; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "field" is empty string', () => { - const payload: Omit & { - field: string; - } = { ...getEntryNestedMock(), field: '' }; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "field" is not a string', () => { - const payload: Omit & { - field: number; - } = { ...getEntryNestedMock(), field: 1 }; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "entries" is not a an array', () => { - const payload: Omit & { - entries: string; - } = { ...getEntryNestedMock(), entries: 'im a string' }; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "im a string" supplied to "entries"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate when "entries" contains an entry item that is type "match"', () => { - const payload = { ...getEntryNestedMock(), entries: [getEntryMatchAnyMock()] }; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ - entries: [ - { - field: 'host.name', - operator: 'included', - type: 'match_any', - value: ['some host name'], - }, - ], - field: 'parent.field', - type: 'nested', - }); - }); - - test('it should validate when "entries" contains an entry item that is type "exists"', () => { - const payload = { ...getEntryNestedMock(), entries: [getEntryExistsMock()] }; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ - entries: [ - { - field: 'host.name', - operator: 'included', - type: 'exists', - }, - ], - field: 'parent.field', - type: 'nested', - }); - }); - - test('it should strip out extra keys', () => { - const payload: EntryNested & { - extraKey?: string; - } = getEntryNestedMock(); - payload.extraKey = 'some extra key'; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryNestedMock()); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.ts deleted file mode 100644 index e0027dab7e071..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../shared_imports'; - -import { nonEmptyNestedEntriesArray } from './non_empty_nested_entries_array'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const entriesNested = t.exact( - t.type({ - entries: nonEmptyNestedEntriesArray, - field: NonEmptyString, - type: t.keyof({ nested: null }), - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type EntryNested = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/index.ts b/x-pack/plugins/lists/common/schemas/types/index.ts deleted file mode 100644 index ebe21174570cb..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './comment'; -export * from './create_comment'; -export * from './update_comment'; -export * from './default_comments_array'; -export * from './default_create_comments_array'; -export * from './default_update_comments_array'; -export * from './default_namespace'; -export * from './entries'; -export * from './entry_match'; -export * from './entry_match_any'; -export * from './entry_match_wildcard'; -export * from './entry_list'; -export * from './entry_exists'; -export * from './entry_nested'; -export * from './non_empty_entries_array'; -export * from './non_empty_or_nullable_string_array'; -export * from './non_empty_nested_entries_array'; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts deleted file mode 100644 index ecd1125589f54..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { getEntryMatchMock } from './entry_match.mock'; -import { getEntryMatchAnyMock } from './entry_match_any.mock'; -import { getEntryExistsMock } from './entry_exists.mock'; -import { getEntryNestedMock } from './entry_nested.mock'; -import { - getEntriesArrayMock, - getListAndNonListEntriesArrayMock, - getListEntriesArrayMock, -} from './entries.mock'; -import { nonEmptyEntriesArray } from './non_empty_entries_array'; -import { EntriesArray } from './entries'; - -describe('non_empty_entries_array', () => { - test('it should FAIL validation when given an empty array', () => { - const payload: EntriesArray = []; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[]" supplied to "NonEmptyEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given "undefined"', () => { - const payload = undefined; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "NonEmptyEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given "null"', () => { - const payload = null; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "null" supplied to "NonEmptyEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate an array of "match" entries', () => { - const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of "match_any" entries', () => { - const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of "exists" entries', () => { - const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of "list" entries', () => { - const payload: EntriesArray = [...getListEntriesArrayMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of "nested" entries', () => { - const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of entries', () => { - const payload: EntriesArray = [...getEntriesArrayMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when given an array of entries of value list and non-value list entries', () => { - const payload: EntriesArray = [...getListAndNonListEntriesArrayMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Cannot have entry of type list and other']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given an array of non entries', () => { - const payload = [1]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "NonEmptyEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.ts deleted file mode 100644 index 89d47c5742b08..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -import { EntriesArray, entriesArray } from './entries'; -import { entriesList } from './entry_list'; - -/** - * Types the nonEmptyEntriesArray as: - * - An array of entries of length 1 or greater - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const nonEmptyEntriesArray = new t.Type( - 'NonEmptyEntriesArray', - entriesArray.is, - (input, context): Either => { - if (Array.isArray(input) && input.length === 0) { - return t.failure(input, context); - } else { - if ( - Array.isArray(input) && - input.some((entry) => entriesList.is(entry)) && - input.some((entry) => !entriesList.is(entry)) - ) { - // fail when an exception item contains both a value list entry and a non-value list entry - return t.failure(input, context, 'Cannot have entry of type list and other'); - } - return entriesArray.validate(input, context); - } - }, - t.identity -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type NonEmptyEntriesArray = t.OutputOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type NonEmptyEntriesArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts deleted file mode 100644 index 20761c7ccdd8a..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { getEntryMatchMock } from './entry_match.mock'; -import { getEntryMatchAnyMock } from './entry_match_any.mock'; -import { getEntryExistsMock } from './entry_exists.mock'; -import { getEntryNestedMock } from './entry_nested.mock'; -import { nonEmptyNestedEntriesArray } from './non_empty_nested_entries_array'; -import { EntriesArray } from './entries'; - -describe('non_empty_nested_entries_array', () => { - test('it should FAIL validation when given an empty array', () => { - const payload: EntriesArray = []; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[]" supplied to "NonEmptyNestedEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given "undefined"', () => { - const payload = undefined; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "NonEmptyNestedEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given "null"', () => { - const payload = null; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "null" supplied to "NonEmptyNestedEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate an array of "match" entries', () => { - const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of "match_any" entries', () => { - const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of "exists" entries', () => { - const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when given an array of "nested" entries', () => { - const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "operator"', - 'Invalid value "nested" supplied to "type"', - 'Invalid value "undefined" supplied to "value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate an array of entries', () => { - const payload: EntriesArray = [ - getEntryExistsMock(), - getEntryMatchAnyMock(), - getEntryMatchMock(), - ]; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when given an array of non entries', () => { - const payload = [1]; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "NonEmptyNestedEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts deleted file mode 100644 index 475183695a559..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -import { entriesMatchAny } from './entry_match_any'; -import { entriesMatch } from './entry_match'; -import { entriesExists } from './entry_exists'; - -export const nestedEntryItem = t.union([entriesMatch, entriesMatchAny, entriesExists]); -export const nestedEntriesArray = t.array(nestedEntryItem); -export type NestedEntriesArray = t.TypeOf; - -/** - * Types the nonEmptyNestedEntriesArray as: - * - An array of entries of length 1 or greater - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const nonEmptyNestedEntriesArray = new t.Type< - NestedEntriesArray, - NestedEntriesArray, - unknown ->( - 'NonEmptyNestedEntriesArray', - nestedEntriesArray.is, - (input, context): Either => { - if (Array.isArray(input) && input.length === 0) { - return t.failure(input, context); - } else { - return nestedEntriesArray.validate(input, context); - } - }, - t.identity -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type NonEmptyNestedEntriesArray = t.OutputOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type NonEmptyNestedEntriesArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts deleted file mode 100644 index d98f5a2a8f82b..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { nonEmptyOrNullableStringArray } from './non_empty_or_nullable_string_array'; - -describe('nonEmptyOrNullableStringArray', () => { - test('it should FAIL validation when given an empty array', () => { - const payload: string[] = []; - const decoded = nonEmptyOrNullableStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[]" supplied to "NonEmptyOrNullableStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given "undefined"', () => { - const payload = undefined; - const decoded = nonEmptyOrNullableStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "NonEmptyOrNullableStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given "null"', () => { - const payload = null; - const decoded = nonEmptyOrNullableStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "null" supplied to "NonEmptyOrNullableStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given an array of with an empty string', () => { - const payload: string[] = ['im good', '']; - const decoded = nonEmptyOrNullableStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "["im good",""]" supplied to "NonEmptyOrNullableStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given an array of non strings', () => { - const payload = [1]; - const decoded = nonEmptyOrNullableStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[1]" supplied to "NonEmptyOrNullableStringArray"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.ts deleted file mode 100644 index ae5a24a250f3a..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the nonEmptyOrNullableStringArray as: - * - An array of non empty strings of length 1 or greater - * - This differs from NonEmptyStringArray in that both input and output are type array - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const nonEmptyOrNullableStringArray = new t.Type( - 'NonEmptyOrNullableStringArray', - t.array(t.string).is, - (input, context): Either => { - const emptyValueFound = Array.isArray(input) && input.some((value) => value === ''); - const nonStringValueFound = - Array.isArray(input) && input.some((value) => typeof value !== 'string'); - - if (Array.isArray(input) && (emptyValueFound || nonStringValueFound || input.length === 0)) { - return t.failure(input, context); - } else { - return t.array(t.string).validate(input, context); - } - }, - t.identity -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type NonEmptyOrNullableStringArray = t.OutputOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type NonEmptyOrNullableStringArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts b/x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts deleted file mode 100644 index 21acc88118039..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { Either, either } from 'fp-ts/lib/Either'; - -export type StringToPositiveNumberC = t.Type; - -/** - * Types the StrongToPositiveNumber as: - * - If a string this converts the string into a number - * - Ensures it is a number (and not NaN) - * - Ensures it is positive number - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const StringToPositiveNumber: StringToPositiveNumberC = new t.Type( - 'StringToPositiveNumber', - t.number.is, - (input, context): Either => { - return either.chain( - t.string.validate(input, context), - (numberAsString): Either => { - const stringAsNumber = +numberAsString; - if (numberAsString.trim().length === 0 || isNaN(stringAsNumber) || stringAsNumber <= 0) { - return t.failure(input, context); - } else { - return t.success(stringAsNumber); - } - } - ); - }, - String -); diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts index b4f3069fb17f9..dea0f9a08fc4c 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { ID } from '../../constants.mock'; +import { UpdateComment, UpdateCommentsArray } from '@kbn/securitysolution-io-ts-utils'; -import { UpdateComment, UpdateCommentsArray } from './update_comment'; +import { ID } from '../../constants.mock'; export const getUpdateCommentMock = (): UpdateComment => ({ comment: 'some comment', diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts deleted file mode 100644 index e0256035ba6e0..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../shared_imports'; - -import { getUpdateCommentMock, getUpdateCommentsArrayMock } from './update_comment.mock'; -import { - UpdateComment, - UpdateCommentsArray, - UpdateCommentsArrayOrUndefined, - updateComment, - updateCommentsArray, - updateCommentsArrayOrUndefined, -} from './update_comment'; - -describe('CommentsUpdate', () => { - describe('updateComment', () => { - test('it should pass validation when supplied typical comment update', () => { - const payload = getUpdateCommentMock(); - const decoded = updateComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation when supplied an undefined for "comment"', () => { - const payload = getUpdateCommentMock(); - // @ts-expect-error - delete payload.comment; - const decoded = updateComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "comment"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when supplied an empty string for "comment"', () => { - const payload = { ...getUpdateCommentMock(), comment: '' }; - const decoded = updateComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "comment"']); - expect(message.schema).toEqual({}); - }); - - test('it should pass validation when supplied an undefined for "id"', () => { - const payload = getUpdateCommentMock(); - delete payload.id; - const decoded = updateComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation when supplied an empty string for "id"', () => { - const payload = { ...getUpdateCommentMock(), id: '' }; - const decoded = updateComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "id"']); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra key passed in', () => { - const payload: UpdateComment & { - extraKey?: string; - } = { ...getUpdateCommentMock(), extraKey: 'some new value' }; - const decoded = updateComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getUpdateCommentMock()); - }); - }); - - describe('updateCommentsArray', () => { - test('it should pass validation when supplied an array of comments', () => { - const payload = getUpdateCommentsArrayMock(); - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation when undefined', () => { - const payload = undefined; - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when array includes non comments types', () => { - const payload = ([1] as unknown) as UpdateCommentsArray; - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('updateCommentsArrayOrUndefined', () => { - test('it should pass validation when supplied an array of comments', () => { - const payload = getUpdateCommentsArrayMock(); - const decoded = updateCommentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should pass validation when supplied when undefined', () => { - const payload = undefined; - const decoded = updateCommentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation when array includes non comments types', () => { - const payload = ([1] as unknown) as UpdateCommentsArrayOrUndefined; - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - }); -}); diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.ts deleted file mode 100644 index 88d9d38688391..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../shared_imports'; -import { id } from '../common/schemas'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const updateComment = t.intersection([ - t.exact( - t.type({ - comment: NonEmptyString, - }) - ), - t.exact( - t.partial({ - id, - }) - ), -]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type UpdateComment = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const updateCommentsArray = t.array(updateComment); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type UpdateCommentsArray = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const updateCommentsArrayOrUndefined = t.union([updateCommentsArray, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type UpdateCommentsArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 8be53cb8cddbc..38eb5aeee8cd2 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -5,17 +5,12 @@ * 2.0. */ +// TODO: We should remove these and instead directly import them in the security_solution project. This is to get my PR across the line without too many conflicts. export { - ListSchema, CommentsArray, - CreateCommentsArray, Comment, CreateComment, - ExceptionListSchema, - ExceptionListItemSchema, - CreateExceptionListSchema, - CreateExceptionListItemSchema, - UpdateExceptionListItemSchema, + CreateCommentsArray, Entry, EntryExists, EntryMatch, @@ -26,15 +21,14 @@ export { EntriesArray, NamespaceType, NestedEntriesArray, - Operator, - OperatorEnum, - OperatorTypeEnum, + ListOperator as Operator, + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, + listOperator as operator, ExceptionListTypeEnum, + ExceptionListType, comment, - exceptionListItemSchema, exceptionListType, - createExceptionListItemSchema, - listSchema, entry, entriesNested, nestedEntryItem, @@ -44,11 +38,22 @@ export { entriesExists, entriesList, namespaceType, - ExceptionListType, - Type, osType, osTypeArray, OsTypeArray, + Type, +} from '@kbn/securitysolution-io-ts-utils'; + +export { + ListSchema, + ExceptionListSchema, + ExceptionListItemSchema, + CreateExceptionListSchema, + CreateExceptionListItemSchema, + UpdateExceptionListItemSchema, + exceptionListItemSchema, + createExceptionListItemSchema, + listSchema, } from './schemas'; export { buildExceptionFilter } from './exceptions'; diff --git a/x-pack/plugins/lists/common/shared_imports.ts b/x-pack/plugins/lists/common/shared_imports.ts deleted file mode 100644 index 2483c1f7dd992..0000000000000 --- a/x-pack/plugins/lists/common/shared_imports.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { - NonEmptyString, - DefaultArray, - DefaultUuid, - DefaultStringArray, - DefaultVersionNumber, - DefaultVersionNumberDecoded, - addIdToItem, - removeIdFromItem, - exactCheck, - getPaths, - foldLeftRight, - validate, - validateEither, - formatErrors, -} from '../../security_solution/common'; diff --git a/x-pack/plugins/lists/common/test_utils.ts b/x-pack/plugins/lists/common/test_utils.ts deleted file mode 100644 index dcf6a2747c3de..0000000000000 --- a/x-pack/plugins/lists/common/test_utils.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { fold } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { formatErrors } from './format_errors'; - -interface Message { - errors: t.Errors; - schema: T | {}; -} - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts - */ -const onLeft = (errors: t.Errors): Message => { - return { errors, schema: {} }; -}; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts - */ -const onRight = (schema: T): Message => { - return { - errors: [], - schema, - }; -}; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts - */ -export const foldLeftRight = fold(onLeft, onRight); - -/** - * Convenience utility to keep the error message handling within tests to be - * very concise. - * @param validation The validation to get the errors from - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts - */ -export const getPaths = (validation: t.Validation): string[] => { - return pipe( - validation, - fold( - (errors) => formatErrors(errors), - () => ['no errors'] - ) - ); -}; - -/** - * Convenience utility to remove text appended to links by EUI - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts - */ -export const removeExternalLinkText = (str: string): string => - str.replace(/\(opens in a new tab or window\)/g, ''); diff --git a/x-pack/plugins/lists/kibana.json b/x-pack/plugins/lists/kibana.json index 1e25fd987552d..ae7b3e7679e0b 100644 --- a/x-pack/plugins/lists/kibana.json +++ b/x-pack/plugins/lists/kibana.json @@ -5,7 +5,7 @@ "kibanaVersion": "kibana", "requiredPlugins": [], "optionalPlugins": ["spaces", "security"], - "requiredBundles": ["securitySolution"], + "requiredBundles": [], "server": true, "ui": true, "version": "8.0.0" diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index ccd917e5ddd8d..e97530da7904a 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -7,8 +7,8 @@ import { chain, fromEither, tryCatch } from 'fp-ts/lib/TaskEither'; import { flow } from 'fp-ts/lib/function'; +import { validateEither } from '@kbn/securitysolution-io-ts-utils'; -import { validateEither } from '../../common/shared_imports'; import { toError, toPromise } from '../common/fp_utils'; import { ENDPOINT_LIST_URL, diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index f0ab7e940f9d8..28d7469d18910 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import styled from 'styled-components'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; @@ -22,7 +23,6 @@ import { AutocompleteFieldMatchAnyComponent } from '../autocomplete/field_value_ import { AutocompleteFieldListsComponent } from '../autocomplete/field_value_lists'; import { ExceptionListType, ListSchema, OperatorTypeEnum } from '../../../../common'; import { getEmptyValue } from '../../../common/empty_value'; -import { OsTypeArray } from '../../../../common/schemas/common'; import { getEntryOnFieldChange, diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx index 11e64630b242d..5f094a64c3660 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx @@ -10,10 +10,10 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; import { AutocompleteStart } from 'src/plugins/data/public'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; import { ExceptionListType } from '../../../../common'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { OsTypeArray } from '../../../../common/schemas'; import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from './types'; import { BuilderAndBadgeComponent } from './and_badge'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index 990831dac6dd9..a698feb93722c 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -9,8 +9,9 @@ import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; +import { addIdToItem } from '@kbn/securitysolution-utils'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; -import { addIdToItem } from '../../../../common/shared_imports'; import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { CreateExceptionListItemSchema, @@ -23,7 +24,6 @@ import { exceptionListItemSchema, } from '../../../../common'; import { AndOrBadge } from '../and_or_badge'; -import { OsTypeArray } from '../../../../common/schemas'; import { CreateExceptionListItemBuilderSchema, ExceptionsBuilderExceptionItem } from './types'; import { BuilderExceptionListItemComponent } from './exception_item_renderer'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts index 209f96742aaf0..6d3bdd09c93ea 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts @@ -6,9 +6,10 @@ */ import uuid from 'uuid'; +import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; +import { OsTypeArray, validate } from '@kbn/securitysolution-io-ts-utils'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; -import { addIdToItem, removeIdFromItem, validate } from '../../../../common/shared_imports'; import { CreateExceptionListItemSchema, EntriesArray, @@ -37,7 +38,6 @@ import { isOperator, } from '../autocomplete/operators'; import { OperatorOption } from '../autocomplete/types'; -import { OsTypeArray } from '../../../../common/schemas'; import { BuilderEntry, diff --git a/x-pack/plugins/lists/public/exceptions/transforms.test.ts b/x-pack/plugins/lists/public/exceptions/transforms.test.ts index 12b0f0bd8624a..c5c43b16d6428 100644 --- a/x-pack/plugins/lists/public/exceptions/transforms.test.ts +++ b/x-pack/plugins/lists/public/exceptions/transforms.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { Entry, EntryMatch, EntryNested } from '@kbn/securitysolution-io-ts-utils'; + import { ExceptionListItemSchema } from '../../common/schemas/response/exception_list_item_schema'; import { UpdateExceptionListItemSchema } from '../../common/schemas/request/update_exception_list_item_schema'; import { CreateExceptionListItemSchema } from '../../common/schemas/request/create_exception_list_item_schema'; @@ -12,7 +14,6 @@ import { getCreateExceptionListItemSchemaMock } from '../../common/schemas/reque import { getUpdateExceptionListItemSchemaMock } from '../../common/schemas/request/update_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../common/schemas/response/exception_list_item_schema.mock'; import { ENTRIES_WITH_IDS } from '../../common/constants.mock'; -import { Entry, EntryMatch, EntryNested } from '../../common/schemas'; import { addIdToExceptionItemEntries, diff --git a/x-pack/plugins/lists/public/exceptions/transforms.ts b/x-pack/plugins/lists/public/exceptions/transforms.ts index 0791760611bf5..468dfc00ca852 100644 --- a/x-pack/plugins/lists/public/exceptions/transforms.ts +++ b/x-pack/plugins/lists/public/exceptions/transforms.ts @@ -6,6 +6,7 @@ */ import { flow } from 'fp-ts/lib/function'; +import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; import { CreateExceptionListItemSchema, @@ -14,7 +15,6 @@ import { ExceptionListItemSchema, UpdateExceptionListItemSchema, } from '../../common'; -import { addIdToItem, removeIdFromItem } from '../../common/shared_imports'; // These are a collection of transforms that are UI specific and useful for UI concerns // that are inserted between the API and the actual user interface. In some ways these diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts index 03cae387711f8..a2842d81a7292 100644 --- a/x-pack/plugins/lists/public/exceptions/types.ts +++ b/x-pack/plugins/lists/public/exceptions/types.ts @@ -5,13 +5,13 @@ * 2.0. */ +import { ExceptionListType, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; + import { CreateExceptionListItemSchema, CreateExceptionListSchema, ExceptionListItemSchema, ExceptionListSchema, - ExceptionListType, - NamespaceType, Page, PerPage, TotalOrUndefined, diff --git a/x-pack/plugins/lists/public/exceptions/utils.ts b/x-pack/plugins/lists/public/exceptions/utils.ts index 009d6e56dc022..6324fdf1df420 100644 --- a/x-pack/plugins/lists/public/exceptions/utils.ts +++ b/x-pack/plugins/lists/public/exceptions/utils.ts @@ -6,10 +6,9 @@ */ import { get } from 'lodash/fp'; +import { NamespaceType, NamespaceTypeArray } from '@kbn/securitysolution-io-ts-utils'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; -import { NamespaceType } from '../../common/schemas'; -import { NamespaceTypeArray } from '../../common/schemas/types/default_namespace_array'; import { SavedObjectType, exceptionListAgnosticSavedObjectType, diff --git a/x-pack/plugins/lists/public/lists/api.ts b/x-pack/plugins/lists/public/lists/api.ts index bfb688250475c..09baa83519fc4 100644 --- a/x-pack/plugins/lists/public/lists/api.ts +++ b/x-pack/plugins/lists/public/lists/api.ts @@ -8,6 +8,7 @@ import { chain, fromEither, map, tryCatch } from 'fp-ts/lib/TaskEither'; import { flow } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; +import { validateEither } from '@kbn/securitysolution-io-ts-utils'; import { AcknowledgeSchema, @@ -30,7 +31,6 @@ import { listSchema, } from '../../common/schemas'; import { LIST_INDEX, LIST_ITEM_URL, LIST_PRIVILEGES_URL, LIST_URL } from '../../common/constants'; -import { validateEither } from '../../common/shared_imports'; import { toError, toPromise } from '../common/fp_utils'; import { diff --git a/x-pack/plugins/lists/public/lists/types.ts b/x-pack/plugins/lists/public/lists/types.ts index 38df9fbf2e37a..6708620439803 100644 --- a/x-pack/plugins/lists/public/lists/types.ts +++ b/x-pack/plugins/lists/public/lists/types.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { Type } from '@kbn/securitysolution-io-ts-utils'; + import { HttpStart } from '../../../../../src/core/public'; -import { Type } from '../../common/schemas'; export interface ApiParams { http: HttpStart; diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts index f972489716641..76ff8b1728922 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { CreateEndpointListItemSchemaDecoded, createEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts index ab295977e98af..23f098f7e9457 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { ENDPOINT_LIST_URL } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { createEndpointListSchema } from '../../common/schemas'; import { getExceptionListClient } from './utils/get_exception_list_client'; diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index 1f6ff5a493f77..4bcb41c666f56 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { CreateExceptionListItemSchemaDecoded, createExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts index c8201dcedbea4..12c887a16c318 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { CreateExceptionListSchemaDecoded, createExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/create_list_index_route.ts b/x-pack/plugins/lists/server/routes/create_list_index_route.ts index db172679d2143..12fe586a07cc0 100644 --- a/x-pack/plugins/lists/server/routes/create_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_index_route.ts @@ -5,9 +5,10 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { LIST_INDEX } from '../../common/constants'; import { acknowledgeSchema } from '../../common/schemas'; diff --git a/x-pack/plugins/lists/server/routes/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_list_item_route.ts index 0491f3511a4cb..2e3c944af0df8 100644 --- a/x-pack/plugins/lists/server/routes/create_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_item_route.ts @@ -5,11 +5,12 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { createListItemSchema, listItemSchema } from '../../common/schemas'; -import { validate } from '../../common/shared_imports'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/create_list_route.ts b/x-pack/plugins/lists/server/routes/create_list_route.ts index 480b9d789708e..4346d519c9003 100644 --- a/x-pack/plugins/lists/server/routes/create_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { CreateListSchemaDecoded, createListSchema, listSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts index 19c0b6c54bb40..195384356f40b 100644 --- a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { DeleteEndpointListItemSchemaDecoded, deleteEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts index bfcaf6046fb6d..ddcd1cf9b7180 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { DeleteExceptionListItemSchemaDecoded, deleteExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts index 90bf6a66f3d6e..f11deef5cb0c8 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { DeleteExceptionListSchemaDecoded, deleteExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts index b6a9b76fd8c94..efad16c37a2dc 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_INDEX } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { acknowledgeSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts index 7345829a8e9b4..a07035fc50d9c 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { deleteListItemSchema, listItemArraySchema, listItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts index 3e9b76a1b330a..65faa54b20cc7 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -5,12 +5,12 @@ * 2.0. */ +import { EntriesArray, validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { - EntriesArray, ExceptionListItemSchema, FoundExceptionListSchema, deleteListSchema, diff --git a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts index 67dc65fcffa43..ee5245982dc0b 100644 --- a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { FindEndpointListItemSchemaDecoded, findEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index 3505b892cf007..82988a7cbeb76 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { FindExceptionListItemSchemaDecoded, findExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts index fc7d58ad9c4bb..4b188b4dca4e2 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { FindExceptionListSchemaDecoded, findExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_list_item_route.ts index d8138792225aa..a904d7f84733d 100644 --- a/x-pack/plugins/lists/server/routes/find_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { FindListItemSchemaDecoded, findListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_list_route.ts b/x-pack/plugins/lists/server/routes/find_list_route.ts index e66a5dd5f6678..c5f1b58c1e957 100644 --- a/x-pack/plugins/lists/server/routes/find_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { findListSchema, foundListSchema } from '../../common/schemas'; import { decodeCursor } from '../services/utils'; diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts index 3049f853692ee..070764b0e1e77 100644 --- a/x-pack/plugins/lists/server/routes/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -6,11 +6,11 @@ */ import { schema } from '@kbn/config-schema'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { importListItemQuerySchema, listSchema } from '../../common/schemas'; import { ConfigType } from '../config'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts index b00e00ba1cdb1..53fd7c65c8ab8 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { listItemSchema, patchListItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts index d8f40474893f4..f139fb72c3066 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { listSchema, patchListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts index dc08dfa30a84c..c78a4a435e5b4 100644 --- a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { ReadEndpointListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts index c41dffcaf98f9..fd92543fa85a7 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { ReadExceptionListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts index 5fc1cdffcd1b1..3d4e831f4a2da 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { ReadExceptionListSchemaDecoded, exceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts index c0bf76c127016..467348669bc0b 100644 --- a/x-pack/plugins/lists/server/routes/read_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_index_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_INDEX } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { listItemIndexExistSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_list_item_route.ts index 5d32b7878b1c2..fd216197f91b5 100644 --- a/x-pack/plugins/lists/server/routes/read_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_item_route.ts @@ -5,11 +5,12 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { listItemArraySchema, listItemSchema, readListItemSchema } from '../../common/schemas'; -import { validate } from '../../common/shared_imports'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_list_route.ts b/x-pack/plugins/lists/server/routes/read_list_route.ts index 91ffeb6f0489e..56acb1e043bd5 100644 --- a/x-pack/plugins/lists/server/routes/read_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { listSchema, readListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts index 101e3acf1c3d0..9f445f4e3c114 100644 --- a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { UpdateEndpointListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index bb2687ff3a575..6a87af6c666bb 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { UpdateExceptionListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index c674ab612a737..a6b99579d87ad 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { UpdateExceptionListSchemaDecoded, exceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_list_item_route.ts index 1fe20a71d46ab..e2905c1a00a11 100644 --- a/x-pack/plugins/lists/server/routes/update_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_item_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { listItemSchema, updateListItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/update_list_route.ts b/x-pack/plugins/lists/server/routes/update_list_route.ts index 0354350ea4574..d69c110aa129b 100644 --- a/x-pack/plugins/lists/server/routes/update_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { validate } from '@kbn/securitysolution-io-ts-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/shared_imports'; import { listSchema, updateListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/validate.ts b/x-pack/plugins/lists/server/routes/validate.ts index 6d03fec7581f8..005d9e85f4853 100644 --- a/x-pack/plugins/lists/server/routes/validate.ts +++ b/x-pack/plugins/lists/server/routes/validate.ts @@ -8,14 +8,18 @@ import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { fold } from 'fp-ts/lib/Either'; +import { + NamespaceType, + NonEmptyEntriesArray, + exactCheck, + formatErrors, + nonEmptyEndpointEntriesArray, + validate, +} from '@kbn/securitysolution-io-ts-utils'; import { ExceptionListClient } from '../services/exception_lists/exception_list_client'; import { MAX_EXCEPTION_LIST_SIZE } from '../../common/constants'; import { foundExceptionListItemSchema } from '../../common/schemas'; -import { NamespaceType, NonEmptyEntriesArray } from '../../common/schemas/types'; -import { nonEmptyEndpointEntriesArray } from '../../common/schemas/types/endpoint'; -import { exactCheck, validate } from '../../common/shared_imports'; -import { formatErrors } from '../siem_server_deps'; export const validateExceptionListSize = async ( exceptionLists: ExceptionListClient, diff --git a/x-pack/plugins/lists/server/saved_objects/migrations.test.ts b/x-pack/plugins/lists/server/saved_objects/migrations.test.ts index f71109b9bb85d..27c883d8b9674 100644 --- a/x-pack/plugins/lists/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/lists/server/saved_objects/migrations.test.ts @@ -9,7 +9,7 @@ import { SavedObjectUnsanitizedDoc } from 'kibana/server'; import uuid from 'uuid'; import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; -import { ExceptionListSoSchema } from '../../common/schemas/saved_objects'; +import { ExceptionListSoSchema } from '../schemas/saved_objects'; import { OldExceptionListSoSchema, migrations } from './migrations'; diff --git a/x-pack/plugins/lists/server/saved_objects/migrations.ts b/x-pack/plugins/lists/server/saved_objects/migrations.ts index 2fa19a6810a8a..316c5f1311774 100644 --- a/x-pack/plugins/lists/server/saved_objects/migrations.ts +++ b/x-pack/plugins/lists/server/saved_objects/migrations.ts @@ -7,16 +7,16 @@ import * as t from 'io-ts'; import { SavedObjectSanitizedDoc, SavedObjectUnsanitizedDoc } from 'kibana/server'; - -import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; import { EntriesArray, - ExceptionListSoSchema, NonEmptyNestedEntriesArray, OsTypeArray, entriesNested, entry, -} from '../../common/schemas'; +} from '@kbn/securitysolution-io-ts-utils'; + +import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; +import { ExceptionListSoSchema } from '../schemas/saved_objects'; const entryType = t.union([entry, entriesNested]); type EntryType = t.TypeOf; diff --git a/x-pack/plugins/lists/common/get_call_cluster.mock.ts b/x-pack/plugins/lists/server/schemas/common/get_call_cluster.mock.ts similarity index 94% rename from x-pack/plugins/lists/common/get_call_cluster.mock.ts rename to x-pack/plugins/lists/server/schemas/common/get_call_cluster.mock.ts index eca73eb305ba7..5ed745c3dbc90 100644 --- a/x-pack/plugins/lists/common/get_call_cluster.mock.ts +++ b/x-pack/plugins/lists/server/schemas/common/get_call_cluster.mock.ts @@ -8,7 +8,8 @@ import { CreateDocumentResponse } from 'elasticsearch'; import { LegacyAPICaller } from 'kibana/server'; -import { LIST_INDEX } from './constants.mock'; +import { LIST_INDEX } from '../../../common/constants.mock'; + import { getShardMock } from './get_shard.mock'; export const getEmptyCreateDocumentResponseMock = (): CreateDocumentResponse => ({ diff --git a/x-pack/plugins/lists/common/get_shard.mock.ts b/x-pack/plugins/lists/server/schemas/common/get_shard.mock.ts similarity index 100% rename from x-pack/plugins/lists/common/get_shard.mock.ts rename to x-pack/plugins/lists/server/schemas/common/get_shard.mock.ts diff --git a/x-pack/plugins/lists/server/schemas/common/schemas.test.ts b/x-pack/plugins/lists/server/schemas/common/schemas.test.ts new file mode 100644 index 0000000000000..75cbc846fbcac --- /dev/null +++ b/x-pack/plugins/lists/server/schemas/common/schemas.test.ts @@ -0,0 +1,288 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { + EsDataTypeGeoPoint, + EsDataTypeGeoPointRange, + EsDataTypeRange, + EsDataTypeRangeTerm, + EsDataTypeSingle, + EsDataTypeUnion, + esDataTypeGeoPoint, + esDataTypeGeoPointRange, + esDataTypeRange, + esDataTypeRangeTerm, + esDataTypeSingle, + esDataTypeUnion, +} from './schemas'; + +describe('esDataTypeUnion', () => { + test('it will work with a regular union', () => { + const payload: EsDataTypeUnion = { boolean: 'true' }; + const decoded = esDataTypeUnion.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will not work with a madeup value', () => { + const payload: EsDataTypeUnion & { madeupValue: 'madeupValue' } = { + boolean: 'true', + madeupValue: 'madeupValue', + }; + const decoded = esDataTypeUnion.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupValue"']); + expect(message.schema).toEqual({}); + }); +}); + +describe('esDataTypeRange', () => { + test('it will work with a given gte, lte range', () => { + const payload: EsDataTypeRange = { gte: '127.0.0.1', lte: '127.0.0.1' }; + const decoded = esDataTypeRange.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will give an error if given an extra madeup value', () => { + const payload: EsDataTypeRange & { madeupvalue: string } = { + gte: '127.0.0.1', + lte: '127.0.0.1', + madeupvalue: 'something', + }; + const decoded = esDataTypeRange.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); + expect(message.schema).toEqual({}); + }); +}); + +describe('esDataTypeRangeTerm', () => { + test('it will work with a date_range', () => { + const payload: EsDataTypeRangeTerm = { date_range: { gte: '2015', lte: '2017' } }; + const decoded = esDataTypeRangeTerm.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will give an error if given an extra madeup value for date_range', () => { + const payload: EsDataTypeRangeTerm & { madeupvalue: string } = { + date_range: { gte: '2015', lte: '2017' }, + madeupvalue: 'something', + }; + const decoded = esDataTypeRangeTerm.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); + expect(message.schema).toEqual({}); + }); + + test('it will work with a double_range', () => { + const payload: EsDataTypeRangeTerm = { double_range: { gte: '2015', lte: '2017' } }; + const decoded = esDataTypeRangeTerm.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will give an error if given an extra madeup value for double_range', () => { + const payload: EsDataTypeRangeTerm & { madeupvalue: string } = { + double_range: { gte: '2015', lte: '2017' }, + madeupvalue: 'something', + }; + const decoded = esDataTypeRangeTerm.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); + expect(message.schema).toEqual({}); + }); + + test('it will work with a float_range', () => { + const payload: EsDataTypeRangeTerm = { float_range: { gte: '2015', lte: '2017' } }; + const decoded = esDataTypeRangeTerm.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will give an error if given an extra madeup value for float_range', () => { + const payload: EsDataTypeRangeTerm & { madeupvalue: string } = { + float_range: { gte: '2015', lte: '2017' }, + madeupvalue: 'something', + }; + const decoded = esDataTypeRangeTerm.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); + expect(message.schema).toEqual({}); + }); + + test('it will work with a integer_range', () => { + const payload: EsDataTypeRangeTerm = { integer_range: { gte: '2015', lte: '2017' } }; + const decoded = esDataTypeRangeTerm.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will give an error if given an extra madeup value for integer_range', () => { + const payload: EsDataTypeRangeTerm & { madeupvalue: string } = { + integer_range: { gte: '2015', lte: '2017' }, + madeupvalue: 'something', + }; + const decoded = esDataTypeRangeTerm.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); + expect(message.schema).toEqual({}); + }); + + test('it will work with a ip_range', () => { + const payload: EsDataTypeRangeTerm = { ip_range: { gte: '127.0.0.1', lte: '127.0.0.2' } }; + const decoded = esDataTypeRangeTerm.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will work with a ip_range as a CIDR', () => { + const payload: EsDataTypeRangeTerm = { ip_range: '127.0.0.1/16' }; + const decoded = esDataTypeRangeTerm.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will give an error if given an extra madeup value for ip_range', () => { + const payload: EsDataTypeRangeTerm & { madeupvalue: string } = { + ip_range: { gte: '127.0.0.1', lte: '127.0.0.2' }, + madeupvalue: 'something', + }; + const decoded = esDataTypeRangeTerm.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); + expect(message.schema).toEqual({}); + }); + + test('it will work with a long_range', () => { + const payload: EsDataTypeRangeTerm = { long_range: { gte: '2015', lte: '2017' } }; + const decoded = esDataTypeRangeTerm.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will give an error if given an extra madeup value for long_range', () => { + const payload: EsDataTypeRangeTerm & { madeupvalue: string } = { + long_range: { gte: '2015', lte: '2017' }, + madeupvalue: 'something', + }; + const decoded = esDataTypeRangeTerm.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); + expect(message.schema).toEqual({}); + }); +}); + +describe('esDataTypeGeoPointRange', () => { + test('it will work with a given lat, lon range', () => { + const payload: EsDataTypeGeoPointRange = { lat: '20', lon: '30' }; + const decoded = esDataTypeGeoPointRange.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will give an error if given an extra madeup value', () => { + const payload: EsDataTypeGeoPointRange & { madeupvalue: string } = { + lat: '20', + lon: '30', + madeupvalue: 'something', + }; + const decoded = esDataTypeGeoPointRange.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); + expect(message.schema).toEqual({}); + }); +}); + +describe('esDataTypeGeoPoint', () => { + test('it will work with a given lat, lon range', () => { + const payload: EsDataTypeGeoPoint = { geo_point: { lat: '127.0.0.1', lon: '127.0.0.1' } }; + const decoded = esDataTypeGeoPoint.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will work with a WKT (Well known text)', () => { + const payload: EsDataTypeGeoPoint = { geo_point: 'POINT (30 10)' }; + const decoded = esDataTypeGeoPoint.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will give an error if given an extra madeup value', () => { + const payload: EsDataTypeGeoPoint & { madeupvalue: string } = { + geo_point: 'POINT (30 10)', + madeupvalue: 'something', + }; + const decoded = esDataTypeGeoPoint.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupvalue"']); + expect(message.schema).toEqual({}); + }); +}); + +describe('esDataTypeSingle', () => { + test('it will work with single type', () => { + const payload: EsDataTypeSingle = { boolean: 'true' }; + const decoded = esDataTypeSingle.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will not work with a madeup value', () => { + const payload: EsDataTypeSingle & { madeupValue: 'madeup' } = { + boolean: 'true', + madeupValue: 'madeup', + }; + const decoded = esDataTypeSingle.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeupValue"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/server/schemas/common/schemas.ts b/x-pack/plugins/lists/server/schemas/common/schemas.ts new file mode 100644 index 0000000000000..86d29f96f2d53 --- /dev/null +++ b/x-pack/plugins/lists/server/schemas/common/schemas.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +export const binary = t.string; +export const binaryOrUndefined = t.union([binary, t.undefined]); + +export const boolean = t.string; +export const booleanOrUndefined = t.union([boolean, t.undefined]); + +export const byte = t.string; +export const byteOrUndefined = t.union([byte, t.undefined]); + +export const date = t.string; +export const dateOrUndefined = t.union([date, t.undefined]); + +export const date_nanos = t.string; +export const dateNanosOrUndefined = t.union([date_nanos, t.undefined]); + +export const double = t.string; +export const doubleOrUndefined = t.union([double, t.undefined]); + +export const float = t.string; +export const floatOrUndefined = t.union([float, t.undefined]); + +export const geo_shape = t.string; +export const geoShapeOrUndefined = t.union([geo_shape, t.undefined]); + +export const half_float = t.string; +export const halfFloatOrUndefined = t.union([half_float, t.undefined]); + +export const integer = t.string; +export const integerOrUndefined = t.union([integer, t.undefined]); + +export const ip = t.string; +export const ipOrUndefined = t.union([ip, t.undefined]); + +export const keyword = t.string; +export const keywordOrUndefined = t.union([keyword, t.undefined]); + +export const text = t.string; +export const textOrUndefined = t.union([text, t.undefined]); + +export const long = t.string; +export const longOrUndefined = t.union([long, t.undefined]); + +export const shape = t.string; +export const shapeOrUndefined = t.union([shape, t.undefined]); + +export const short = t.string; +export const shortOrUndefined = t.union([short, t.undefined]); + +export const esDataTypeRange = t.exact(t.type({ gte: t.string, lte: t.string })); + +export const date_range = esDataTypeRange; +export const dateRangeOrUndefined = t.union([date_range, t.undefined]); + +export const double_range = esDataTypeRange; +export const doubleRangeOrUndefined = t.union([double_range, t.undefined]); + +export const float_range = esDataTypeRange; +export const floatRangeOrUndefined = t.union([float_range, t.undefined]); + +export const integer_range = esDataTypeRange; +export const integerRangeOrUndefined = t.union([integer_range, t.undefined]); + +// ip_range can be just a CIDR value as a range +export const ip_range = t.union([esDataTypeRange, t.string]); +export const ipRangeOrUndefined = t.union([ip_range, t.undefined]); + +export const long_range = esDataTypeRange; +export const longRangeOrUndefined = t.union([long_range, t.undefined]); + +export type EsDataTypeRange = t.TypeOf; + +export const esDataTypeRangeTerm = t.union([ + t.exact(t.type({ date_range })), + t.exact(t.type({ double_range })), + t.exact(t.type({ float_range })), + t.exact(t.type({ integer_range })), + t.exact(t.type({ ip_range })), + t.exact(t.type({ long_range })), +]); + +export type EsDataTypeRangeTerm = t.TypeOf; + +export const esDataTypeGeoPointRange = t.exact(t.type({ lat: t.string, lon: t.string })); +export type EsDataTypeGeoPointRange = t.TypeOf; + +export const geo_point = t.union([esDataTypeGeoPointRange, t.string]); +export type GeoPoint = t.TypeOf; + +export const geoPointOrUndefined = t.union([geo_point, t.undefined]); + +export const esDataTypeGeoPoint = t.exact(t.type({ geo_point })); +export type EsDataTypeGeoPoint = t.TypeOf; + +export const esDataTypeGeoShape = t.union([ + t.exact(t.type({ geo_shape: t.string })), + t.exact(t.type({ shape: t.string })), +]); + +export type EsDataTypeGeoShape = t.TypeOf; + +export const esDataTypeSingle = t.union([ + t.exact(t.type({ binary })), + t.exact(t.type({ boolean })), + t.exact(t.type({ byte })), + t.exact(t.type({ date })), + t.exact(t.type({ date_nanos })), + t.exact(t.type({ double })), + t.exact(t.type({ float })), + t.exact(t.type({ half_float })), + t.exact(t.type({ integer })), + t.exact(t.type({ ip })), + t.exact(t.type({ keyword })), + t.exact(t.type({ long })), + t.exact(t.type({ short })), + t.exact(t.type({ text })), +]); + +export type EsDataTypeSingle = t.TypeOf; + +export const esDataTypeUnion = t.union([ + esDataTypeRangeTerm, + esDataTypeGeoPoint, + esDataTypeGeoShape, + esDataTypeSingle, +]); + +export type EsDataTypeUnion = t.TypeOf; + +export const _index = t.string; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.ts b/x-pack/plugins/lists/server/schemas/elastic_query/create_es_bulk_type.ts similarity index 100% rename from x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.ts rename to x-pack/plugins/lists/server/schemas/elastic_query/create_es_bulk_type.ts diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index.ts b/x-pack/plugins/lists/server/schemas/elastic_query/index.ts similarity index 100% rename from x-pack/plugins/lists/common/schemas/elastic_query/index.ts rename to x-pack/plugins/lists/server/schemas/elastic_query/index.ts index 3d7fcadd42d01..23f74859a6577 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_query/index.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -export * from './update_es_list_schema'; +export * from './create_es_bulk_type'; +export * from './index_es_list_item_schema'; export * from './index_es_list_schema'; export * from './update_es_list_item_schema'; -export * from './index_es_list_item_schema'; -export * from './create_es_bulk_type'; +export * from './update_es_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.mock.ts b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.mock.ts similarity index 90% rename from x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.mock.ts rename to x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.mock.ts index 938af6b6c5e0c..9a41946c6677c 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.mock.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { IndexEsListItemSchema } from '../../../common/schemas'; import { DATE_NOW, LIST_ID, META, TIE_BREAKER, USER, VALUE } from '../../../common/constants.mock'; +import { IndexEsListItemSchema } from './index_es_list_item_schema'; + export const getIndexESListItemMock = (ip = VALUE): IndexEsListItemSchema => ({ created_at: DATE_NOW, created_by: USER, diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts similarity index 86% rename from x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts rename to x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts index da8a490e444ab..696434a616c53 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts @@ -6,19 +6,21 @@ */ import * as t from 'io-ts'; - import { created_at, created_by, + metaOrUndefined, + updated_at, + updated_by, +} from '@kbn/securitysolution-io-ts-utils'; + +import { esDataTypeUnion } from '../common/schemas'; +import { deserializerOrUndefined, - esDataTypeUnion, list_id, - metaOrUndefined, serializerOrUndefined, tie_breaker_id, - updated_at, - updated_by, -} from '../common/schemas'; +} from '../../../common/schemas'; export const indexEsListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.mock.ts b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.mock.ts similarity index 92% rename from x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.mock.ts rename to x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.mock.ts index bd7171a6552c2..9e72c83223bab 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.mock.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.mock.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { IndexEsListSchema } from '../../../common/schemas'; import { DATE_NOW, DESCRIPTION, @@ -18,6 +17,8 @@ import { VERSION, } from '../../../common/constants.mock'; +import { IndexEsListSchema } from './index_es_list_schema'; + export const getIndexESListMock = (): IndexEsListSchema => ({ created_at: DATE_NOW, created_by: USER, diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts similarity index 91% rename from x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts rename to x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts index 4fd89ca731368..c69abaf785dec 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts @@ -6,22 +6,24 @@ */ import * as t from 'io-ts'; - import { created_at, created_by, description, - deserializerOrUndefined, - immutable, metaOrUndefined, name, - serializerOrUndefined, - tie_breaker_id, type, updated_at, updated_by, +} from '@kbn/securitysolution-io-ts-utils'; + +import { + deserializerOrUndefined, + immutable, + serializerOrUndefined, + tie_breaker_id, version, -} from '../common/schemas'; +} from '../../../common/schemas'; export const indexEsListSchema = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts similarity index 78% rename from x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts rename to x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts index 2808873dec67f..1f49943a910bc 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts @@ -6,8 +6,9 @@ */ import * as t from 'io-ts'; +import { metaOrUndefined, updated_at, updated_by } from '@kbn/securitysolution-io-ts-utils'; -import { esDataTypeUnion, metaOrUndefined, updated_at, updated_by } from '../common/schemas'; +import { esDataTypeUnion } from '../common/schemas'; export const updateEsListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts similarity index 93% rename from x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts rename to x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts index 5612aaddf18b0..fbeac92c66bdd 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts @@ -6,14 +6,13 @@ */ import * as t from 'io-ts'; - import { descriptionOrUndefined, metaOrUndefined, nameOrUndefined, updated_at, updated_by, -} from '../common/schemas'; +} from '@kbn/securitysolution-io-ts-utils'; export const updateEsListSchema = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/index.ts b/x-pack/plugins/lists/server/schemas/elastic_response/index.ts similarity index 100% rename from x-pack/plugins/lists/common/schemas/elastic_response/index.ts rename to x-pack/plugins/lists/server/schemas/elastic_response/index.ts diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.mock.ts b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.mock.ts similarity index 93% rename from x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.mock.ts rename to x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.mock.ts index 4a1f9dab17540..de49e822f7dc8 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.mock.ts @@ -7,7 +7,6 @@ import { SearchResponse } from 'elasticsearch'; -import { SearchEsListItemSchema } from '../../../common/schemas'; import { DATE_NOW, LIST_ID, @@ -18,7 +17,9 @@ import { USER, VALUE, } from '../../../common/constants.mock'; -import { getShardMock } from '../../get_shard.mock'; +import { getShardMock } from '../common/get_shard.mock'; + +import { SearchEsListItemSchema } from './search_es_list_item_schema'; export const getSearchEsListItemsAsAllUndefinedMock = (): SearchEsListItemSchema => ({ binary: undefined, diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.test.ts similarity index 94% rename from x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts rename to x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.test.ts index 26cb66735bb0b..5206335554a6b 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.test.ts @@ -7,8 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { SearchEsListItemSchema, searchEsListItemSchema } from './search_es_list_item_schema'; import { getSearchEsListItemMock } from './search_es_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts similarity index 95% rename from x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.ts rename to x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts index 05dc175303759..8ac88a1610ea7 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts @@ -6,17 +6,21 @@ */ import * as t from 'io-ts'; +import { + created_at, + created_by, + metaOrUndefined, + updated_at, + updated_by, +} from '@kbn/securitysolution-io-ts-utils'; import { binaryOrUndefined, booleanOrUndefined, byteOrUndefined, - created_at, - created_by, dateNanosOrUndefined, dateOrUndefined, dateRangeOrUndefined, - deserializerOrUndefined, doubleOrUndefined, doubleRangeOrUndefined, floatOrUndefined, @@ -29,18 +33,18 @@ import { ipOrUndefined, ipRangeOrUndefined, keywordOrUndefined, - list_id, longOrUndefined, longRangeOrUndefined, - metaOrUndefined, - serializerOrUndefined, shapeOrUndefined, shortOrUndefined, textOrUndefined, - tie_breaker_id, - updated_at, - updated_by, } from '../common/schemas'; +import { + deserializerOrUndefined, + list_id, + serializerOrUndefined, + tie_breaker_id, +} from '../../../common/schemas'; export const searchEsListItemSchema = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.mock.ts b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.mock.ts similarity index 92% rename from x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.mock.ts rename to x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.mock.ts index 05ab914f5238d..07d8c92f79932 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.mock.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.mock.ts @@ -7,7 +7,6 @@ import { SearchResponse } from 'elasticsearch'; -import { SearchEsListSchema } from '../../../common/schemas'; import { DATE_NOW, DESCRIPTION, @@ -21,7 +20,9 @@ import { USER, VERSION, } from '../../../common/constants.mock'; -import { getShardMock } from '../../get_shard.mock'; +import { getShardMock } from '../common/get_shard.mock'; + +import { SearchEsListSchema } from './search_es_list_schema'; export const getSearchEsListMock = (): SearchEsListSchema => ({ created_at: DATE_NOW, diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.test.ts similarity index 94% rename from x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts rename to x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.test.ts index 80d4d885b87bc..4500c0ed68dcc 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.test.ts @@ -7,8 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; - -import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { SearchEsListSchema, searchEsListSchema } from './search_es_list_schema'; import { getSearchEsListMock } from './search_es_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts similarity index 91% rename from x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.ts rename to x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts index 37fe3147b57df..a060ebda04a46 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts @@ -6,22 +6,24 @@ */ import * as t from 'io-ts'; - import { created_at, created_by, description, - deserializerOrUndefined, - immutable, metaOrUndefined, name, - serializerOrUndefined, - tie_breaker_id, type, updated_at, updated_by, +} from '@kbn/securitysolution-io-ts-utils'; + +import { + deserializerOrUndefined, + immutable, + serializerOrUndefined, + tie_breaker_id, version, -} from '../common/schemas'; +} from '../../../common/schemas'; export const searchEsListSchema = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/saved_objects/exceptions_list_so_schema.ts b/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts similarity index 89% rename from x-pack/plugins/lists/common/schemas/saved_objects/exceptions_list_so_schema.ts rename to x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts index 01865e04bfe45..f6d2e891a60d0 100644 --- a/x-pack/plugins/lists/common/schemas/saved_objects/exceptions_list_so_schema.ts +++ b/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts @@ -6,26 +6,29 @@ */ import * as t from 'io-ts'; - -import { commentsArrayOrUndefined, entriesArrayOrUndefined } from '../types'; import { + commentsArrayOrUndefined, created_at, created_by, description, + entriesArrayOrUndefined, exceptionListItemType, exceptionListType, - immutableOrUndefined, - itemIdOrUndefined, - list_id, - list_type, metaOrUndefined, name, osTypeArray, tags, - tie_breaker_id, updated_by, +} from '@kbn/securitysolution-io-ts-utils'; + +import { + immutableOrUndefined, + itemIdOrUndefined, + list_id, + list_type, + tie_breaker_id, versionOrUndefined, -} from '../common/schemas'; +} from '../../../common/schemas'; /** * Superset saved object of both lists and list items since they share the same saved object type. diff --git a/x-pack/plugins/lists/common/schemas/saved_objects/index.ts b/x-pack/plugins/lists/server/schemas/saved_objects/index.ts similarity index 100% rename from x-pack/plugins/lists/common/schemas/saved_objects/index.ts rename to x-pack/plugins/lists/server/schemas/saved_objects/index.ts diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts index 95e9df03400af..dba729437b814 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts @@ -13,7 +13,8 @@ import { ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_EVENT_FILTERS_LIST_NAME, } from '../../../common/constants'; -import { ExceptionListSchema, ExceptionListSoSchema, Version } from '../../../common/schemas'; +import { ExceptionListSchema, Version } from '../../../common/schemas'; +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts index bf573f7527614..de2be0cb72735 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts @@ -13,7 +13,8 @@ import { ENDPOINT_LIST_ID, ENDPOINT_LIST_NAME, } from '../../../common/constants'; -import { ExceptionListSchema, ExceptionListSoSchema, Version } from '../../../common/schemas'; +import { ExceptionListSchema, Version } from '../../../common/schemas'; +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_trusted_apps_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_trusted_apps_list.ts index 2e56c6edfed0e..6f6ad7c357f14 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_trusted_apps_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_trusted_apps_list.ts @@ -13,7 +13,8 @@ import { ENDPOINT_TRUSTED_APPS_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_NAME, } from '../../../common/constants'; -import { ExceptionListSchema, ExceptionListSoSchema, Version } from '../../../common/schemas'; +import { ExceptionListSchema, Version } from '../../../common/schemas'; +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts index f7dd57dfba55a..ef4ceb2f12922 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts @@ -7,20 +7,17 @@ import { SavedObjectsClientContract } from 'kibana/server'; import uuid from 'uuid'; - import { Description, - ExceptionListSchema, - ExceptionListSoSchema, ExceptionListType, - Immutable, - ListId, MetaOrUndefined, Name, NamespaceType, Tags, - Version, -} from '../../../common/schemas'; +} from '@kbn/securitysolution-io-ts-utils'; + +import { ExceptionListSchema, Immutable, ListId, Version } from '../../../common/schemas'; +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts index 219686617d493..5f88244171f6a 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts @@ -7,22 +7,20 @@ import { SavedObjectsClientContract } from 'kibana/server'; import uuid from 'uuid'; - import { CreateCommentsArray, Description, EntriesArray, - ExceptionListItemSchema, ExceptionListItemType, - ExceptionListSoSchema, - ItemId, - ListId, MetaOrUndefined, Name, NamespaceType, OsTypeArray, Tags, -} from '../../../common/schemas'; +} from '@kbn/securitysolution-io-ts-utils'; + +import { ExceptionListItemSchema, ItemId, ListId } from '../../../common/schemas'; +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectType, diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts index eb9509d631c4f..afe9106e28d82 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts @@ -6,13 +6,9 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; +import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; -import { - ExceptionListSchema, - IdOrUndefined, - ListIdOrUndefined, - NamespaceType, -} from '../../../common/schemas'; +import { ExceptionListSchema, ListIdOrUndefined } from '../../../common/schemas'; import { getSavedObjectType } from './utils'; import { getExceptionList } from './get_exception_list'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts index d89ff4d2a9ff0..d0e1d2283cc6f 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts @@ -6,14 +6,9 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; +import { Id, IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; -import { - ExceptionListItemSchema, - Id, - IdOrUndefined, - ItemIdOrUndefined, - NamespaceType, -} from '../../../common/schemas'; +import { ExceptionListItemSchema, ItemIdOrUndefined } from '../../../common/schemas'; import { getSavedObjectType } from './utils'; import { getExceptionListItem } from './get_exception_list_item'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts index 43ef45d326699..d9ec08b818f2d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts @@ -5,8 +5,10 @@ * 2.0. */ +import { NamespaceType } from '@kbn/securitysolution-io-ts-utils'; + import { SavedObjectsClientContract } from '../../../../../../src/core/server/'; -import { ListId, NamespaceType } from '../../../common/schemas'; +import { ListId } from '../../../common/schemas'; import { findExceptionListItem } from './find_exception_list_item'; import { getSavedObjectType } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index 86587baee5fb3..0954a55d44dcc 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -6,39 +6,41 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; - -import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array'; -import { NonEmptyStringArrayDecoded } from '../../../common/schemas/types/non_empty_string_array'; -import { EmptyStringArrayDecoded } from '../../../common/schemas/types/empty_string_array'; import { CreateCommentsArray, Description, DescriptionOrUndefined, + EmptyStringArrayDecoded, EntriesArray, ExceptionListItemType, ExceptionListItemTypeOrUndefined, ExceptionListType, ExceptionListTypeOrUndefined, - FilterOrUndefined, Id, IdOrUndefined, - Immutable, - ItemId, - ItemIdOrUndefined, - ListId, - ListIdOrUndefined, MetaOrUndefined, Name, NameOrUndefined, NamespaceType, + NamespaceTypeArray, + NonEmptyStringArrayDecoded, OsTypeArray, + Tags, + TagsOrUndefined, + UpdateCommentsArray, +} from '@kbn/securitysolution-io-ts-utils'; + +import { + FilterOrUndefined, + Immutable, + ItemId, + ItemIdOrUndefined, + ListId, + ListIdOrUndefined, PageOrUndefined, PerPageOrUndefined, SortFieldOrUndefined, SortOrderOrUndefined, - Tags, - TagsOrUndefined, - UpdateCommentsArray, Version, VersionOrUndefined, _VersionOrUndefined, diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts index 794457c80f047..82ea5a4f104c5 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts @@ -6,11 +6,10 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; +import { NamespaceTypeArray } from '@kbn/securitysolution-io-ts-utils'; -import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array'; import { SavedObjectType } from '../../../common/types'; import { - ExceptionListSoSchema, FilterOrUndefined, FoundExceptionListSchema, PageOrUndefined, @@ -18,6 +17,7 @@ import { SortFieldOrUndefined, SortOrderOrUndefined, } from '../../../common/schemas'; +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectTypes, transformSavedObjectsToFoundExceptionList } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts index ec51ae089d961..cb9c16ffe3c7b 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts @@ -6,12 +6,12 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; +import { NamespaceType } from '@kbn/securitysolution-io-ts-utils'; import { FilterOrUndefined, FoundExceptionListItemSchema, ListId, - NamespaceType, PageOrUndefined, PerPageOrUndefined, SortFieldOrUndefined, diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts index 155408dafc79d..6721aff5b0c1e 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts @@ -6,25 +6,27 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; +import { + EmptyStringArrayDecoded, + Id, + NamespaceTypeArray, + NonEmptyStringArrayDecoded, +} from '@kbn/securitysolution-io-ts-utils'; import { SavedObjectType, exceptionListAgnosticSavedObjectType, exceptionListSavedObjectType, } from '../../../common/types'; -import { EmptyStringArrayDecoded } from '../../../common/schemas/types/empty_string_array'; -import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array'; -import { NonEmptyStringArrayDecoded } from '../../../common/schemas/types/non_empty_string_array'; import { - ExceptionListSoSchema, FoundExceptionListItemSchema, - Id, PageOrUndefined, PerPageOrUndefined, SortFieldOrUndefined, SortOrderOrUndefined, } from '../../../common/schemas'; import { escapeQuotes } from '../utils/escape_query'; +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectTypes, transformSavedObjectsToFoundExceptionListItem } from './utils'; import { getExceptionList } from './get_exception_list'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts index 16fa4fd30fd37..342e03160b45b 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts @@ -5,17 +5,14 @@ * 2.0. */ +import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; + import { SavedObjectsClientContract, SavedObjectsErrorHelpers, } from '../../../../../../src/core/server/'; -import { - ExceptionListSchema, - ExceptionListSoSchema, - IdOrUndefined, - ListIdOrUndefined, - NamespaceType, -} from '../../../common/schemas'; +import { ExceptionListSchema, ListIdOrUndefined } from '../../../common/schemas'; +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts index 99fdd23ccf9c4..cf469baa46370 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts @@ -5,17 +5,14 @@ * 2.0. */ +import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; + import { SavedObjectsClientContract, SavedObjectsErrorHelpers, } from '../../../../../../src/core/server/'; -import { - ExceptionListItemSchema, - ExceptionListSoSchema, - IdOrUndefined, - ItemIdOrUndefined, - NamespaceType, -} from '../../../common/schemas'; +import { ExceptionListItemSchema, ItemIdOrUndefined } from '../../../common/schemas'; +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectType, transformSavedObjectToExceptionListItem } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts index e81e119b0cf17..69d9b87227bca 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts @@ -6,22 +6,24 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; - import { DescriptionOrUndefined, - ExceptionListSchema, - ExceptionListSoSchema, ExceptionListTypeOrUndefined, IdOrUndefined, - ListIdOrUndefined, MetaOrUndefined, NameOrUndefined, NamespaceType, OsTypeArray, TagsOrUndefined, +} from '@kbn/securitysolution-io-ts-utils'; + +import { + ExceptionListSchema, + ListIdOrUndefined, VersionOrUndefined, _VersionOrUndefined, } from '../../../common/schemas'; +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectType, transformSavedObjectUpdateToExceptionList } from './utils'; import { getExceptionList } from './get_exception_list'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts index e1bed459006cc..041008a06f3df 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts @@ -6,23 +6,25 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; - import { DescriptionOrUndefined, EntriesArray, - ExceptionListItemSchema, ExceptionListItemTypeOrUndefined, - ExceptionListSoSchema, IdOrUndefined, - ItemIdOrUndefined, MetaOrUndefined, NameOrUndefined, NamespaceType, OsTypeArray, TagsOrUndefined, UpdateCommentsArrayOrUndefined, +} from '@kbn/securitysolution-io-ts-utils'; + +import { + ExceptionListItemSchema, + ItemIdOrUndefined, _VersionOrUndefined, } from '../../../common/schemas'; +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectType, diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index 5d9525ae1030f..1322f153bf3bd 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -7,27 +7,29 @@ import uuid from 'uuid'; import { SavedObject, SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server'; +import { + CommentsArray, + CreateComment, + CreateCommentsArray, + NamespaceType, + NamespaceTypeArray, + UpdateCommentsArrayOrUndefined, + exceptionListItemType, + exceptionListType, +} from '@kbn/securitysolution-io-ts-utils'; import { SavedObjectType, exceptionListAgnosticSavedObjectType, exceptionListSavedObjectType, } from '../../../common/types'; -import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array'; import { - CommentsArray, - CreateComment, - CreateCommentsArray, ExceptionListItemSchema, ExceptionListSchema, - ExceptionListSoSchema, FoundExceptionListItemSchema, FoundExceptionListSchema, - NamespaceType, - UpdateCommentsArrayOrUndefined, - exceptionListItemType, - exceptionListType, } from '../../../common/schemas'; +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; export const getSavedObjectType = ({ namespaceType, diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts index acfed44c5259e..d601f7f3eff45 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts @@ -9,8 +9,8 @@ import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; -import { getIndexESListItemMock } from '../../../common/schemas/elastic_query/index_es_list_item_schema.mock'; import { LIST_ITEM_ID, LIST_ITEM_INDEX } from '../../../common/constants.mock'; +import { getIndexESListItemMock } from '../../schemas/elastic_query/index_es_list_item_schema.mock'; import { CreateListItemOptions, createListItem } from './create_list_item'; import { getCreateListItemOptionsMock } from './create_list_item.mock'; diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index cf8a43be796df..3c51f56c7916a 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -7,18 +7,16 @@ import uuid from 'uuid'; import { ElasticsearchClient } from 'kibana/server'; +import { IdOrUndefined, MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-utils'; import { DeserializerOrUndefined, - IdOrUndefined, - IndexEsListItemSchema, ListItemSchema, - MetaOrUndefined, SerializerOrUndefined, - Type, } from '../../../common/schemas'; import { transformListItemToElasticQuery } from '../utils'; import { encodeHitVersion } from '../utils/encode_hit_version'; +import { IndexEsListItemSchema } from '../../schemas/elastic_query'; export interface CreateListItemOptions { deserializer: DeserializerOrUndefined; diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts index f9f9728798a0b..ea2ff697c7d37 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { getIndexESListItemMock } from '../../../common/schemas/elastic_query/index_es_list_item_schema.mock'; import { LIST_ITEM_INDEX, TIE_BREAKERS, VALUE_2 } from '../../../common/constants.mock'; +import { getIndexESListItemMock } from '../../schemas/elastic_query/index_es_list_item_schema.mock'; import { CreateListItemsBulkOptions, createListItemsBulk } from './create_list_items_bulk'; import { getCreateListItemBulkOptionsMock } from './create_list_items_bulk.mock'; diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts index 86d8d9a698b1f..5928260ab94ac 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts @@ -7,16 +7,11 @@ import uuid from 'uuid'; import { ElasticsearchClient } from 'kibana/server'; +import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-utils'; import { transformListItemToElasticQuery } from '../utils'; -import { - CreateEsBulkTypeSchema, - DeserializerOrUndefined, - IndexEsListItemSchema, - MetaOrUndefined, - SerializerOrUndefined, - Type, -} from '../../../common/schemas'; +import { DeserializerOrUndefined, SerializerOrUndefined } from '../../../common/schemas'; +import { CreateEsBulkTypeSchema, IndexEsListItemSchema } from '../../schemas/elastic_query'; export interface CreateListItemsBulkOptions { deserializer: DeserializerOrUndefined; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts index f2e9949c82c3e..4fcb2656d2ba7 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts @@ -6,8 +6,9 @@ */ import { ElasticsearchClient } from 'kibana/server'; +import { Id } from '@kbn/securitysolution-io-ts-utils'; -import { Id, ListItemSchema } from '../../../common/schemas'; +import { ListItemSchema } from '../../../common/schemas'; import { getListItem } from '.'; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts index 1c7ac3afb3ee3..ccbe8d6fe7925 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts @@ -6,8 +6,9 @@ */ import { ElasticsearchClient } from 'kibana/server'; +import { Type } from '@kbn/securitysolution-io-ts-utils'; -import { ListItemArraySchema, Type } from '../../../common/schemas'; +import { ListItemArraySchema } from '../../../common/schemas'; import { getQueryFilterFromTypeValue } from '../utils'; import { getListItemByValues } from './get_list_item_by_values'; diff --git a/x-pack/plugins/lists/server/services/items/find_list_item.mock.ts b/x-pack/plugins/lists/server/services/items/find_list_item.mock.ts index 4bf62982b2a9f..c00da8ab2496b 100644 --- a/x-pack/plugins/lists/server/services/items/find_list_item.mock.ts +++ b/x-pack/plugins/lists/server/services/items/find_list_item.mock.ts @@ -9,8 +9,8 @@ import { Client } from 'elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; -import { getShardMock } from '../../../common/get_shard.mock'; import { LIST_ID, LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock'; +import { getShardMock } from '../../schemas/common/get_shard.mock'; import { FindListItemOptions } from './find_list_item'; diff --git a/x-pack/plugins/lists/server/services/items/find_list_item.test.ts b/x-pack/plugins/lists/server/services/items/find_list_item.test.ts index c76d1c505df0c..098c8d20ae5ce 100644 --- a/x-pack/plugins/lists/server/services/items/find_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/find_list_item.test.ts @@ -8,9 +8,9 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; -import { getShardMock } from '../../../common/get_shard.mock'; import { getFoundListItemSchemaMock } from '../../../common/schemas/response/found_list_item_schema.mock'; -import { getEmptySearchListMock } from '../../../common/schemas/elastic_response/search_es_list_schema.mock'; +import { getShardMock } from '../../schemas/common/get_shard.mock'; +import { getEmptySearchListMock } from '../../schemas/elastic_response/search_es_list_schema.mock'; import { getFindListItemOptionsMock } from './find_list_item.mock'; import { findListItem } from './find_list_item'; diff --git a/x-pack/plugins/lists/server/services/items/find_list_item.ts b/x-pack/plugins/lists/server/services/items/find_list_item.ts index 3e37ccb0cfb1f..e1586daf1cbb1 100644 --- a/x-pack/plugins/lists/server/services/items/find_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/find_list_item.ts @@ -13,10 +13,10 @@ import { ListId, Page, PerPage, - SearchEsListItemSchema, SortFieldOrUndefined, SortOrderOrUndefined, } from '../../../common/schemas'; +import { SearchEsListItemSchema } from '../../schemas/elastic_response'; import { getList } from '../lists'; import { encodeCursor, diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts index f92031cae02ca..f6ec534a39ebb 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts @@ -8,7 +8,6 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; -import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; import { DATE_NOW, @@ -18,6 +17,7 @@ import { TIE_BREAKER, USER, } from '../../../common/constants.mock'; +import { getSearchListItemMock } from '../../schemas/elastic_response/search_es_list_item_schema.mock'; import { getListItem } from './get_list_item'; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.ts b/x-pack/plugins/lists/server/services/items/get_list_item.ts index 519ebaedfddbc..aca8deac24817 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts @@ -6,10 +6,12 @@ */ import { ElasticsearchClient } from 'kibana/server'; +import { Id } from '@kbn/securitysolution-io-ts-utils'; -import { Id, ListItemSchema, SearchEsListItemSchema } from '../../../common/schemas'; +import { ListItemSchema } from '../../../common/schemas'; import { transformElasticToListItem } from '../utils'; import { findSourceType } from '../utils/find_source_type'; +import { SearchEsListItemSchema } from '../../schemas/elastic_response'; interface GetListItemOptions { id: Id; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts index 7d3fe81babe59..083dca2ea9410 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts @@ -6,8 +6,9 @@ */ import { ElasticsearchClient } from 'kibana/server'; +import { Type } from '@kbn/securitysolution-io-ts-utils'; -import { ListItemArraySchema, Type } from '../../../common/schemas'; +import { ListItemArraySchema } from '../../../common/schemas'; import { getListItemByValues } from '.'; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts index aa22049ce6fe4..f3c72794b6dd9 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts @@ -8,7 +8,6 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; -import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; import { DATE_NOW, LIST_ID, @@ -21,6 +20,7 @@ import { VALUE, VALUE_2, } from '../../../common/constants.mock'; +import { getSearchListItemMock } from '../../schemas/elastic_response/search_es_list_item_schema.mock'; import { getListItemByValues } from './get_list_item_by_values'; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts index c00ee2b13426a..5a4d55172af23 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts @@ -6,13 +6,15 @@ */ import { ElasticsearchClient } from 'kibana/server'; +import { Type } from '@kbn/securitysolution-io-ts-utils'; -import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; +import { ListItemArraySchema } from '../../../common/schemas'; import { TransformElasticToListItemOptions, getQueryFilterFromTypeValue, transformElasticToListItem, } from '../utils'; +import { SearchEsListItemSchema } from '../../schemas/elastic_response'; export interface GetListItemByValuesOptions { listId: string; diff --git a/x-pack/plugins/lists/server/services/items/index.ts b/x-pack/plugins/lists/server/services/items/index.ts index 01df8b4a7d9f1..346c64ba68fc0 100644 --- a/x-pack/plugins/lists/server/services/items/index.ts +++ b/x-pack/plugins/lists/server/services/items/index.ts @@ -21,3 +21,5 @@ export * from './update_list_item'; export * from './write_lines_to_bulk_list_items'; export * from './write_list_items_to_stream'; export * from './search_list_item_by_values'; +export * from './update_list_item'; +export * from './write_list_items_to_stream'; diff --git a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.test.ts b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.test.ts index 0d084c50b5745..817432495926b 100644 --- a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.test.ts +++ b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.test.ts @@ -9,8 +9,8 @@ import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { SearchListItemArraySchema } from '../../../common/schemas'; -import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2 } from '../../../common/constants.mock'; +import { getSearchListItemMock } from '../../schemas/elastic_response/search_es_list_item_schema.mock'; import { searchListItemByValues } from './search_list_item_by_values'; diff --git a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts index 4f8808d06e425..d6d8f66770653 100644 --- a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts +++ b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts @@ -6,13 +6,15 @@ */ import { ElasticsearchClient } from 'kibana/server'; +import { Type } from '@kbn/securitysolution-io-ts-utils'; -import { SearchEsListItemSchema, SearchListItemArraySchema, Type } from '../../../common/schemas'; +import { SearchListItemArraySchema } from '../../../common/schemas'; import { TransformElasticMSearchToListItemOptions, getQueryFilterFromTypeValue, transformElasticNamedSearchToListItem, } from '../utils'; +import { SearchEsListItemSchema } from '../../schemas/elastic_response'; export interface SearchListItemByValuesOptions { listId: string; diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index 89c7e77707d8f..91c38dd3f331c 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -6,17 +6,13 @@ */ import { ElasticsearchClient } from 'kibana/server'; +import { Id, MetaOrUndefined } from '@kbn/securitysolution-io-ts-utils'; -import { - Id, - ListItemSchema, - MetaOrUndefined, - UpdateEsListItemSchema, - _VersionOrUndefined, -} from '../../../common/schemas'; +import { ListItemSchema, _VersionOrUndefined } from '../../../common/schemas'; import { transformListItemToElasticQuery } from '../utils'; import { decodeVersion } from '../utils/decode_version'; import { encodeHitVersion } from '../utils/encode_hit_version'; +import { UpdateEsListItemSchema } from '../../schemas/elastic_query'; import { getListItem } from './get_list_item'; diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts index 8450890cfa355..8a05e4667a290 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts @@ -8,15 +8,14 @@ import { Readable } from 'stream'; import { ElasticsearchClient } from 'kibana/server'; +import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-utils'; import { createListIfItDoesNotExist } from '../lists/create_list_if_it_does_not_exist'; import { DeserializerOrUndefined, ListIdOrUndefined, ListSchema, - MetaOrUndefined, SerializerOrUndefined, - Type, Version, } from '../../../common/schemas'; import { ConfigType } from '../../config'; diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts index ee4f3af9cdd5c..e6d0b21ab0517 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts @@ -8,8 +8,8 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; -import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; import { LIST_ID, LIST_ITEM_INDEX } from '../../../common/constants.mock'; +import { getSearchListItemMock } from '../../schemas/elastic_response/search_es_list_item_schema.mock'; import { getExportListItemsToStreamOptionsMock, diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts index 3679680ad79bd..a70db9aba3880 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts @@ -10,9 +10,9 @@ import { PassThrough } from 'stream'; import type { estypes } from '@elastic/elasticsearch'; import { ElasticsearchClient } from 'kibana/server'; -import { SearchEsListItemSchema } from '../../../common/schemas'; import { ErrorWithStatusCode } from '../../error_with_status_code'; import { findSourceValue } from '../utils/find_source_value'; +import { SearchEsListItemSchema } from '../../schemas/elastic_response'; /** * How many results to page through from the network at a time diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_streams.mock.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_streams.mock.ts index 3de8fdb0c9df6..8961d7527ad0b 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_streams.mock.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_streams.mock.ts @@ -10,7 +10,6 @@ import { Stream } from 'stream'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; -import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; import { ExportListItemsToStreamOptions, GetResponseOptions, @@ -18,6 +17,7 @@ import { WriteResponseHitsToStreamOptions, } from '../items'; import { LIST_ID, LIST_ITEM_INDEX } from '../../../common/constants.mock'; +import { getSearchListItemMock } from '../../schemas/elastic_response/search_es_list_item_schema.mock'; export const getExportListItemsToStreamOptionsMock = (): ExportListItemsToStreamOptions => ({ esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, diff --git a/x-pack/plugins/lists/server/services/lists/create_list.test.ts b/x-pack/plugins/lists/server/services/lists/create_list.test.ts index e6213a1c6eabe..600d148d77b95 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.test.ts @@ -10,8 +10,8 @@ import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mo import { ListSchema } from '../../../common/schemas'; import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; -import { getIndexESListMock } from '../../../common/schemas/elastic_query/index_es_list_schema.mock'; import { LIST_ID, LIST_INDEX } from '../../../common/constants.mock'; +import { getIndexESListMock } from '../../schemas/elastic_query/index_es_list_schema.mock'; import { CreateListOptions, createList } from './create_list'; import { getCreateListOptionsMock } from './create_list.mock'; diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index baed699dc992f..6b0954f3fcc9d 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -7,21 +7,23 @@ import uuid from 'uuid'; import { ElasticsearchClient } from 'kibana/server'; +import { + Description, + IdOrUndefined, + MetaOrUndefined, + Name, + Type, +} from '@kbn/securitysolution-io-ts-utils'; import { encodeHitVersion } from '../utils/encode_hit_version'; import { - Description, DeserializerOrUndefined, - IdOrUndefined, Immutable, - IndexEsListSchema, ListSchema, - MetaOrUndefined, - Name, SerializerOrUndefined, - Type, Version, } from '../../../common/schemas'; +import { IndexEsListSchema } from '../../schemas/elastic_query'; export interface CreateListOptions { id: IdOrUndefined; diff --git a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts index 5325d951626c7..483810a9b1c43 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts @@ -6,17 +6,13 @@ */ import { ElasticsearchClient } from 'kibana/server'; +import { Description, Id, MetaOrUndefined, Name, Type } from '@kbn/securitysolution-io-ts-utils'; import { - Description, DeserializerOrUndefined, - Id, Immutable, ListSchema, - MetaOrUndefined, - Name, SerializerOrUndefined, - Type, Version, } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts index 4fe200bff436f..0e140544fa47d 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.ts @@ -6,8 +6,9 @@ */ import { ElasticsearchClient } from 'kibana/server'; +import { Id } from '@kbn/securitysolution-io-ts-utils'; -import { Id, ListSchema } from '../../../common/schemas'; +import { ListSchema } from '../../../common/schemas'; import { getList } from './get_list'; diff --git a/x-pack/plugins/lists/server/services/lists/find_list.ts b/x-pack/plugins/lists/server/services/lists/find_list.ts index 9c61d36dc0cd3..92d7262c19543 100644 --- a/x-pack/plugins/lists/server/services/lists/find_list.ts +++ b/x-pack/plugins/lists/server/services/lists/find_list.ts @@ -12,10 +12,10 @@ import { FoundListSchema, Page, PerPage, - SearchEsListSchema, SortFieldOrUndefined, SortOrderOrUndefined, } from '../../../common/schemas'; +import { SearchEsListSchema } from '../../schemas/elastic_response'; import { encodeCursor, getQueryFilter, diff --git a/x-pack/plugins/lists/server/services/lists/get_list.test.ts b/x-pack/plugins/lists/server/services/lists/get_list.test.ts index 930a52266ba41..f599e5ef8b6c5 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.test.ts @@ -8,9 +8,9 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; -import { getSearchListMock } from '../../../common/schemas/elastic_response/search_es_list_schema.mock'; import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; import { LIST_ID, LIST_INDEX } from '../../../common/constants.mock'; +import { getSearchListMock } from '../../schemas/elastic_response/search_es_list_schema.mock'; import { getList } from './get_list'; diff --git a/x-pack/plugins/lists/server/services/lists/get_list.ts b/x-pack/plugins/lists/server/services/lists/get_list.ts index 6f18d143df00b..a248f81449bfc 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.ts @@ -6,9 +6,11 @@ */ import { ElasticsearchClient } from 'kibana/server'; +import { Id } from '@kbn/securitysolution-io-ts-utils'; -import { Id, ListSchema, SearchEsListSchema } from '../../../common/schemas'; +import { ListSchema } from '../../../common/schemas'; import { transformElasticToList } from '../utils/transform_elastic_to_list'; +import { SearchEsListSchema } from '../../schemas/elastic_response'; interface GetListOptions { id: Id; diff --git a/x-pack/plugins/lists/server/services/lists/index.ts b/x-pack/plugins/lists/server/services/lists/index.ts index f19103a558c1c..c12868816d91c 100644 --- a/x-pack/plugins/lists/server/services/lists/index.ts +++ b/x-pack/plugins/lists/server/services/lists/index.ts @@ -8,7 +8,9 @@ export * from './create_list'; export * from './delete_list'; export * from './find_list'; -export * from './get_list'; +export * from './get_list_index'; export * from './get_list_template'; +export * from './get_list'; +export * from './list_client'; +export * from './types'; export * from './update_list'; -export * from './get_list_index'; diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts index 1efcd2af5420e..b684511ff679c 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts @@ -8,26 +8,28 @@ import { PassThrough, Readable } from 'stream'; import { ElasticsearchClient } from 'kibana/server'; - import { Description, DescriptionOrUndefined, - DeserializerOrUndefined, - Filter, Id, IdOrUndefined, - Immutable, - ListId, - ListIdOrUndefined, MetaOrUndefined, Name, NameOrUndefined, + Type, +} from '@kbn/securitysolution-io-ts-utils'; + +import { + DeserializerOrUndefined, + Filter, + Immutable, + ListId, + ListIdOrUndefined, Page, PerPage, SerializerOrUndefined, SortFieldOrUndefined, SortOrderOrUndefined, - Type, Version, VersionOrUndefined, _VersionOrUndefined, diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index f98e40b04b6d7..4917fec7397ea 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -6,19 +6,17 @@ */ import { ElasticsearchClient } from 'kibana/server'; - -import { decodeVersion } from '../utils/decode_version'; -import { encodeHitVersion } from '../utils/encode_hit_version'; import { DescriptionOrUndefined, Id, - ListSchema, MetaOrUndefined, NameOrUndefined, - UpdateEsListSchema, - VersionOrUndefined, - _VersionOrUndefined, -} from '../../../common/schemas'; +} from '@kbn/securitysolution-io-ts-utils'; + +import { decodeVersion } from '../utils/decode_version'; +import { encodeHitVersion } from '../utils/encode_hit_version'; +import { ListSchema, VersionOrUndefined, _VersionOrUndefined } from '../../../common/schemas'; +import { UpdateEsListSchema } from '../../schemas/elastic_query'; import { getList } from '.'; diff --git a/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts index 1692f76727fac..a1a349d5e38da 100644 --- a/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts +++ b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts @@ -8,9 +8,9 @@ import * as t from 'io-ts'; import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; +import { exactCheck } from '@kbn/securitysolution-io-ts-utils'; import { CursorOrUndefined, SortFieldOrUndefined } from '../../../common/schemas'; -import { exactCheck } from '../../../common/shared_imports'; /** * Used only internally for this ad-hoc opaque cursor structure to keep track of the diff --git a/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts b/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts index 5cba4928420cb..e408f7d33b548 100644 --- a/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts +++ b/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts @@ -5,8 +5,10 @@ * 2.0. */ -import { getSearchEsListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; -import { SearchEsListItemSchema, Type } from '../../../common/schemas'; +import { Type } from '@kbn/securitysolution-io-ts-utils'; + +import { getSearchEsListItemMock } from '../../schemas/elastic_response/search_es_list_item_schema.mock'; +import { SearchEsListItemSchema } from '../../schemas/elastic_response'; import { findSourceType } from './find_source_type'; diff --git a/x-pack/plugins/lists/server/services/utils/find_source_type.ts b/x-pack/plugins/lists/server/services/utils/find_source_type.ts index 904b2982ea757..00a6985b2c751 100644 --- a/x-pack/plugins/lists/server/services/utils/find_source_type.ts +++ b/x-pack/plugins/lists/server/services/utils/find_source_type.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { SearchEsListItemSchema, Type, type } from '../../../common/schemas'; +import { Type, type } from '@kbn/securitysolution-io-ts-utils'; + +import { SearchEsListItemSchema } from '../../schemas/elastic_response'; export const findSourceType = ( listItem: SearchEsListItemSchema, diff --git a/x-pack/plugins/lists/server/services/utils/find_source_value.test.ts b/x-pack/plugins/lists/server/services/utils/find_source_value.test.ts index f071be81dbae1..59226a2643140 100644 --- a/x-pack/plugins/lists/server/services/utils/find_source_value.test.ts +++ b/x-pack/plugins/lists/server/services/utils/find_source_value.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { SearchEsListItemSchema } from '../../../common/schemas'; -import { getSearchEsListItemsAsAllUndefinedMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; +import { SearchEsListItemSchema } from '../../schemas/elastic_response'; +import { getSearchEsListItemsAsAllUndefinedMock } from '../../schemas/elastic_response/search_es_list_item_schema.mock'; import { DEFAULT_DATE_RANGE, diff --git a/x-pack/plugins/lists/server/services/utils/find_source_value.ts b/x-pack/plugins/lists/server/services/utils/find_source_value.ts index 4f882d590780f..c12f4bdfcdb9f 100644 --- a/x-pack/plugins/lists/server/services/utils/find_source_value.ts +++ b/x-pack/plugins/lists/server/services/utils/find_source_value.ts @@ -6,14 +6,11 @@ */ import Mustache from 'mustache'; +import { type } from '@kbn/securitysolution-io-ts-utils'; -import { - DeserializerOrUndefined, - SearchEsListItemSchema, - esDataTypeGeoPointRange, - esDataTypeRange, - type, -} from '../../../common/schemas'; +import { DeserializerOrUndefined } from '../../../common/schemas'; +import { SearchEsListItemSchema } from '../../schemas/elastic_response'; +import { esDataTypeGeoPointRange, esDataTypeRange } from '../../schemas/common/schemas'; export const DEFAULT_GEO_POINT = '{{{lat}}},{{{lon}}}'; export const DEFAULT_DATE_RANGE = '{{{gte}}},{{{lte}}}'; diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts index 46524565770c6..0ece97b21d5b7 100644 --- a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts @@ -6,8 +6,7 @@ */ import { isEmpty, isObject } from 'lodash/fp'; - -import { Type } from '../../../common/schemas'; +import { Type } from '@kbn/securitysolution-io-ts-utils'; export type QueryFilterType = [ { term: Record }, diff --git a/x-pack/plugins/lists/server/services/utils/index.ts b/x-pack/plugins/lists/server/services/utils/index.ts index 78beb5cc8e37d..0cd2720bd199b 100644 --- a/x-pack/plugins/lists/server/services/utils/index.ts +++ b/x-pack/plugins/lists/server/services/utils/index.ts @@ -6,17 +6,20 @@ */ export * from './calculate_scroll_math'; +export * from './decode_version'; export * from './encode_decode_cursor'; +export * from './encode_hit_version'; +export * from './escape_query'; export * from './find_source_type'; export * from './find_source_value'; -export * from './get_query_filter'; export * from './get_query_filter_from_type_value'; +export * from './get_query_filter'; export * from './get_search_after_scroll'; export * from './get_search_after_with_tie_breaker'; export * from './get_sort_with_tie_breaker'; export * from './get_source_with_tie_breaker'; export * from './scroll_to_start_page'; export * from './transform_elastic_named_search_to_list_item'; -export * from './transform_elastic_to_list'; export * from './transform_elastic_to_list_item'; +export * from './transform_elastic_to_list'; export * from './transform_list_item_to_elastic_query'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.test.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.test.ts index df9e6c57cecc8..1846f1b7909fb 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.test.ts @@ -7,11 +7,11 @@ import { getSearchListItemResponseMock } from '../../../common/schemas/response/search_list_item_schema.mock'; import { LIST_INDEX, LIST_ITEM_ID, TYPE, VALUE } from '../../../common/constants.mock'; +import { SearchListItemArraySchema } from '../../../common/schemas'; import { getSearchEsListItemMock, getSearchListItemMock, -} from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; -import { SearchListItemArraySchema } from '../../../common/schemas'; +} from '../../schemas/elastic_response/search_es_list_item_schema.mock'; import { transformElasticNamedSearchToListItem } from './transform_elastic_named_search_to_list_item'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts index 4f0f8fe49a9d0..f02ae17fa0293 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts @@ -6,8 +6,10 @@ */ import type { estypes } from '@elastic/elasticsearch'; +import { Type } from '@kbn/securitysolution-io-ts-utils'; -import { SearchEsListItemSchema, SearchListItemArraySchema, Type } from '../../../common/schemas'; +import { SearchListItemArraySchema } from '../../../common/schemas'; +import { SearchEsListItemSchema } from '../../schemas/elastic_response'; import { transformElasticHitsToListItem } from './transform_elastic_to_list_item'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts index 4ed08f70219af..8d8c076f6e219 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts @@ -7,7 +7,8 @@ import type { estypes } from '@elastic/elasticsearch'; -import { ListArraySchema, SearchEsListSchema } from '../../../common/schemas'; +import { ListArraySchema } from '../../../common/schemas'; +import { SearchEsListSchema } from '../../schemas/elastic_response'; import { encodeHitVersion } from './encode_hit_version'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts index 5ca9a26844207..3629881f61d5a 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; import { ListItemArraySchema } from '../../../common/schemas'; +import { getSearchListItemMock } from '../../schemas/elastic_response/search_es_list_item_schema.mock'; import { transformElasticHitsToListItem, diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts index 436987e71dd22..3e27bd24517e4 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts @@ -6,9 +6,11 @@ */ import type { estypes } from '@elastic/elasticsearch'; +import { Type } from '@kbn/securitysolution-io-ts-utils'; -import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; +import { ListItemArraySchema } from '../../../common/schemas'; import { ErrorWithStatusCode } from '../../error_with_status_code'; +import { SearchEsListItemSchema } from '../../schemas/elastic_response'; import { encodeHitVersion } from './encode_hit_version'; import { findSourceValue } from './find_source_value'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts index fae857484dcf5..f697d93f28561 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EsDataTypeUnion } from '../../../common/schemas'; +import { EsDataTypeUnion } from '../../schemas/common/schemas'; import { DEFAULT_DATE_REGEX, diff --git a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts index 957c5311bc8e2..32eb885871cb1 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts @@ -5,18 +5,19 @@ * 2.0. */ +import { Type } from '@kbn/securitysolution-io-ts-utils'; + +import { SerializerOrUndefined } from '../../../common/schemas'; import { EsDataTypeGeoPoint, EsDataTypeGeoShape, EsDataTypeRangeTerm, EsDataTypeSingle, EsDataTypeUnion, - SerializerOrUndefined, - Type, esDataTypeGeoShape, esDataTypeRangeTerm, esDataTypeSingle, -} from '../../../common/schemas'; +} from '../../schemas/common/schemas'; export const DEFAULT_DATE_REGEX = RegExp('(?.+),(?.+)|(?.+)'); export const DEFAULT_LTE_GTE_REGEX = RegExp('(?.+)-(?.+)|(?.+)'); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index 0226114487a81..bf15994f60cbc 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -21,7 +21,8 @@ import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_e import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import * as helpers from '../helpers'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { EntriesArray } from '../../../../../../lists/common/schemas/types'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; + import { ExceptionListItemSchema } from '../../../../../../lists/common'; import { getRulesEqlSchemaMock, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index a97e71de77abd..7ee0e6888a42e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -20,7 +20,7 @@ import { import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { EntriesArray } from '../../../../../../lists/common/schemas/types'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; import { getRulesEqlSchemaMock, getRulesSchemaMock, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 7f7a587997062..0560b790e4047 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -56,11 +56,10 @@ import { ENTRIES_WITH_IDS, OLD_DATE_RELATIVE_TO_DATE_NOW, } from '../../../../../lists/common/constants.mock'; +import { EntriesArray, OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; import { CreateExceptionListItemSchema, ExceptionListItemSchema, - EntriesArray, - OsTypeArray, } from '../../../../../lists/common/schemas'; import { IFieldType, IIndexPattern } from 'src/plugins/data/common'; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index 88bf7941c8464..ee6962f7e9535 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -9,7 +9,7 @@ import { ExceptionListClient } from '../../../../../lists/server'; import { listMock } from '../../../../../lists/server/mocks'; import { getFoundExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { EntriesArray, EntryList } from '../../../../../lists/common/schemas/types'; +import { EntriesArray, EntryList } from '@kbn/securitysolution-io-ts-utils'; import { buildArtifact, getEndpointExceptionList, diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index 54b6971eec58e..7a5b906860f10 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -7,10 +7,10 @@ import { createHash } from 'crypto'; import { deflate } from 'zlib'; +import { Entry, EntryNested } from '@kbn/securitysolution-io-ts-utils'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { validate } from '../../../../common/validate'; -import { Entry, EntryNested } from '../../../../../lists/common/schemas/types'; import { ExceptionListClient } from '../../../../../lists/server'; import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../common/shared_imports'; import { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts index 1320122626bfd..58df4b3f11412 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts @@ -4,8 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import type { estypes } from '@elastic/elasticsearch'; -import { ExceptionListItemSchema, entriesList } from '../../../../../../lists/common/schemas'; +import { entriesList } from '@kbn/securitysolution-io-ts-utils'; + +import { ExceptionListItemSchema } from '../../../../../../lists/common/schemas'; import { hasLargeValueList } from '../../../../../common/detection_engine/utils'; import { FilterEventsAgainstListOptions } from './types'; import { filterEvents } from './filter_events'; From c787495f0045919952af54c7a4e05751503369f5 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 10 May 2021 10:01:19 -0700 Subject: [PATCH 14/69] Use doc link services in rollups (#99137) --- ...-plugin-core-public.doclinksstart.links.md | 1 + ...kibana-plugin-core-public.doclinksstart.md | 4 +++- .../public/doc_links/doc_links_service.ts | 2 ++ src/core/public/public.api.md | 1 + .../job_create/steps/step_date_histogram.js | 6 ++--- .../job_create/steps/step_histogram.js | 4 ++-- .../job_create/steps/step_logistics.js | 6 ++--- .../sections/job_create/steps/step_metrics.js | 4 ++-- .../sections/job_create/steps/step_terms.js | 4 ++-- .../crud_app/services/documentation_links.js | 23 ------------------- .../crud_app/services/documentation_links.ts | 14 +++++++++++ .../rollup/public/crud_app/services/index.js | 13 ++--------- x-pack/plugins/rollup/public/plugin.ts | 4 ++-- .../job_create_clone.test.js | 5 ++-- .../job_create_date_histogram.test.js | 5 ++-- .../job_create_histogram.test.js | 5 ++-- .../job_create_logistics.test.js | 5 ++-- .../job_create_metrics.test.js | 5 ++-- .../job_create_terms.test.js | 5 ++-- 19 files changed, 55 insertions(+), 61 deletions(-) delete mode 100644 x-pack/plugins/rollup/public/crud_app/services/documentation_links.js create mode 100644 x-pack/plugins/rollup/public/crud_app/services/documentation_links.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 180d376ceaf51..0448ad42c94fa 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -144,6 +144,7 @@ readonly links: { createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; + createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 91ef8358b5fd2..ac625095da2a4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,7 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | + +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | + diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 581d614c9a371..e1bb42023caab 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -319,6 +319,7 @@ export class DocLinksService { createSnapshotLifecyclePolicy: `${ELASTICSEARCH_DOCS}slm-api-put-policy.html`, createRoleMapping: `${ELASTICSEARCH_DOCS}security-api-put-role-mapping.html`, createRoleMappingTemplates: `${ELASTICSEARCH_DOCS}security-api-put-role-mapping.html#_role_templates`, + createRollupJobsRequest: `${ELASTICSEARCH_DOCS}rollup-put-job.html#rollup-put-job-api-request-body`, createApiKey: `${ELASTICSEARCH_DOCS}security-api-create-api-key.html`, createPipeline: `${ELASTICSEARCH_DOCS}put-pipeline-api.html`, createTransformRequest: `${ELASTICSEARCH_DOCS}put-transform.html#put-transform-request-body`, @@ -544,6 +545,7 @@ export interface DocLinksStart { createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; + createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 0523c523baf6f..4ea3b56c60a8f 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -629,6 +629,7 @@ export interface DocLinksStart { createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; + createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_date_histogram.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_date_histogram.js index 0c8561e273700..f36f8aa65b9f3 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_date_histogram.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_date_histogram.js @@ -28,7 +28,7 @@ import { import { search } from '../../../../../../../../src/plugins/data/public'; const { parseEsInterval } = search.aggs; -import { getDateHistogramDetailsUrl, getDateHistogramAggregationUrl } from '../../../services'; +import { documentationLinks } from '../../../services/documentation_links'; import { StepError } from './components'; @@ -194,7 +194,7 @@ export class StepDateHistogram extends Component { + - + `${esBase}/rollup-job-config.html#_logistical_details`; -export const getDateHistogramDetailsUrl = () => - `${esBase}/rollup-job-config.html#_date_histogram_2`; -export const getTermsDetailsUrl = () => `${esBase}/rollup-job-config.html#_terms_2`; -export const getHistogramDetailsUrl = () => `${esBase}/rollup-job-config.html#_histogram_2`; -export const getMetricsDetailsUrl = () => `${esBase}/rollup-job-config.html#rollup-metrics-config`; - -export const getDateHistogramAggregationUrl = () => - `${esBase}/search-aggregations-bucket-datehistogram-aggregation.html`; -export const getCronUrl = () => `${esBase}/trigger-schedule.html#_cron_expressions`; diff --git a/x-pack/plugins/rollup/public/crud_app/services/documentation_links.ts b/x-pack/plugins/rollup/public/crud_app/services/documentation_links.ts new file mode 100644 index 0000000000000..65bbfd919f94d --- /dev/null +++ b/x-pack/plugins/rollup/public/crud_app/services/documentation_links.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DocLinksStart } from 'src/core/public'; + +export let documentationLinks: DocLinksStart['links']; + +export const init = (docLinks: DocLinksStart) => { + documentationLinks = docLinks.links; +}; diff --git a/x-pack/plugins/rollup/public/crud_app/services/index.js b/x-pack/plugins/rollup/public/crud_app/services/index.js index a1dddb774b806..948847fa7e68a 100644 --- a/x-pack/plugins/rollup/public/crud_app/services/index.js +++ b/x-pack/plugins/rollup/public/crud_app/services/index.js @@ -11,17 +11,6 @@ export { showApiError, showApiWarning } from './api_errors'; export { listBreadcrumb, createBreadcrumb } from './breadcrumbs'; -export { - setEsBaseAndXPackBase, - getLogisticalDetailsUrl, - getDateHistogramDetailsUrl, - getDateHistogramAggregationUrl, - getTermsDetailsUrl, - getHistogramDetailsUrl, - getMetricsDetailsUrl, - getCronUrl, -} from './documentation_links'; - export { filterItems } from './filter_items'; export { flattenPanelTree } from './flatten_panel_tree'; @@ -47,3 +36,5 @@ export { sortTable } from './sort_table'; export { retypeMetrics } from './retype_metrics'; export { METRIC_TYPE } from './track_ui_metric'; + +export { init } from './documentation_links'; diff --git a/x-pack/plugins/rollup/public/plugin.ts b/x-pack/plugins/rollup/public/plugin.ts index 120e93ab7026c..17e352e1a4472 100644 --- a/x-pack/plugins/rollup/public/plugin.ts +++ b/x-pack/plugins/rollup/public/plugin.ts @@ -21,7 +21,7 @@ import { ManagementSetup } from '../../../../src/plugins/management/public'; import { IndexManagementPluginSetup } from '../../index_management/public'; import { IndexPatternManagementSetup } from '../../../../src/plugins/index_pattern_management/public'; // @ts-ignore -import { setEsBaseAndXPackBase, setHttp } from './crud_app/services/index'; +import { setHttp, init as initDocumentation } from './crud_app/services/index'; import { setNotifications, setFatalErrors, setUiStatsReporter } from './kibana_services'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; @@ -108,6 +108,6 @@ export class RollupPlugin implements Plugin { start(core: CoreStart) { setHttp(core.http); setNotifications(core.notifications); - setEsBaseAndXPackBase(core.docLinks.ELASTIC_WEBSITE_URL, core.docLinks.DOC_LINK_VERSION); + initDocumentation(core.docLinks); } } diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_clone.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_clone.test.js index 361a0dba1a8a2..40c26baa76441 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_clone.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_clone.test.js @@ -5,10 +5,10 @@ * 2.0. */ -import { setHttp } from '../../crud_app/services'; +import { setHttp, init as initDocumentation } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers, nextTick } from './helpers'; import { JOB_TO_CLONE, JOB_CLONE_INDEX_PATTERN_CHECK } from './helpers/constants'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { coreMock, docLinksServiceMock } from '../../../../../../src/core/public/mocks'; jest.mock('lodash', () => ({ ...jest.requireActual('lodash'), @@ -31,6 +31,7 @@ describe('Cloning a rollup job through create job wizard', () => { beforeAll(() => { startMock = coreMock.createStart(); setHttp(startMock.http); + initDocumentation(docLinksServiceMock.createStartContract()); }); beforeEach(() => { diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_date_histogram.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_date_histogram.test.js index 8ffcb1207aca3..88c72140bcdda 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_date_histogram.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_date_histogram.test.js @@ -7,9 +7,9 @@ import moment from 'moment-timezone'; -import { setHttp } from '../../crud_app/services'; +import { setHttp, init as initDocumentation } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers } from './helpers'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { docLinksServiceMock, coreMock } from '../../../../../../src/core/public/mocks'; jest.mock('lodash', () => ({ ...jest.requireActual('lodash'), @@ -30,6 +30,7 @@ describe('Create Rollup Job, step 2: Date histogram', () => { beforeAll(() => { startMock = coreMock.createStart(); setHttp(startMock.http); + initDocumentation(docLinksServiceMock.createStartContract()); }); beforeEach(() => { // Set "default" mock responses by not providing any arguments diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_histogram.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_histogram.test.js index 96af67ad6fbb3..7d9d714ba8d2d 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_histogram.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_histogram.test.js @@ -5,9 +5,9 @@ * 2.0. */ -import { setHttp } from '../../crud_app/services'; +import { setHttp, init as initDocumentation } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers } from './helpers'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { coreMock, docLinksServiceMock } from '../../../../../../src/core/public/mocks'; jest.mock('lodash', () => ({ ...jest.requireActual('lodash'), @@ -29,6 +29,7 @@ describe('Create Rollup Job, step 4: Histogram', () => { beforeAll(() => { startMock = coreMock.createStart(); setHttp(startMock.http); + initDocumentation(docLinksServiceMock.createStartContract()); }); beforeEach(() => { diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js index 1c54a42cbee63..792cb4434e551 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js @@ -6,9 +6,9 @@ */ import { indexPatterns } from '../../../../../../src/plugins/data/public'; -import { setHttp } from '../../crud_app/services'; +import { setHttp, init as initDocumentation } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers } from './helpers'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { coreMock, docLinksServiceMock } from '../../../../../../src/core/public/mocks'; jest.mock('lodash', () => ({ ...jest.requireActual('lodash'), @@ -28,6 +28,7 @@ describe('Create Rollup Job, step 1: Logistics', () => { beforeAll(() => { startMock = coreMock.createStart(); setHttp(startMock.http); + initDocumentation(docLinksServiceMock.createStartContract()); }); beforeEach(() => { diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_metrics.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_metrics.test.js index a6d4e16e3e232..97dbc90b84cc2 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_metrics.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_metrics.test.js @@ -5,9 +5,9 @@ * 2.0. */ -import { setHttp } from '../../crud_app/services'; +import { setHttp, init as initDocumentation } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers } from './helpers'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { coreMock, docLinksServiceMock } from '../../../../../../src/core/public/mocks'; jest.mock('lodash', () => ({ ...jest.requireActual('lodash'), @@ -29,6 +29,7 @@ describe('Create Rollup Job, step 5: Metrics', () => { beforeAll(() => { startMock = coreMock.createStart(); setHttp(startMock.http); + initDocumentation(docLinksServiceMock.createStartContract()); }); beforeEach(() => { diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_terms.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_terms.test.js index eb8c054776019..d27eab75b7758 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_terms.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_terms.test.js @@ -5,9 +5,9 @@ * 2.0. */ -import { setHttp } from '../../crud_app/services'; +import { setHttp, init as initDocumentation } from '../../crud_app/services'; import { pageHelpers, mockHttpRequest } from './helpers'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { coreMock, docLinksServiceMock } from '../../../../../../src/core/public/mocks'; jest.mock('lodash', () => ({ ...jest.requireActual('lodash'), @@ -28,6 +28,7 @@ describe('Create Rollup Job, step 3: Terms', () => { beforeAll(() => { startMock = coreMock.createStart(); setHttp(startMock.http); + initDocumentation(docLinksServiceMock.createStartContract()); }); beforeEach(() => { From 16e1414ae01be8ad183dd7b06d23fff58938b507 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 10 May 2021 19:05:33 +0200 Subject: [PATCH 15/69] add codeowners for xpack.banners (#99630) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bafa023cf3f35..de323128afed1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -188,6 +188,7 @@ /src/core/ @elastic/kibana-core /src/plugins/saved_objects_tagging_oss @elastic/kibana-core /config/kibana.yml @elastic/kibana-core +/x-pack/plugins/banners/ @elastic/kibana-core /x-pack/plugins/features/ @elastic/kibana-core /x-pack/plugins/licensing/ @elastic/kibana-core /x-pack/plugins/global_search/ @elastic/kibana-core From dfe8637c52d1c270d8eb783a733e6ea7c3aa1a1c Mon Sep 17 00:00:00 2001 From: Dan Panzarella Date: Mon, 10 May 2021 13:31:11 -0400 Subject: [PATCH 16/69] [Security Solution] Add Host Isolation API (#98842) --- .../common/endpoint/constants.ts | 3 +- .../common/endpoint/generate_data.ts | 12 + .../common/endpoint/schema/actions.ts | 18 + .../common/endpoint/types/actions.ts | 27 ++ .../common/endpoint/types/index.ts | 7 + .../security_solution/common/license/index.ts | 8 + .../public/common/hooks/use_license.ts | 2 +- .../containers/detection_engine/alerts/api.ts | 6 +- .../detection_engine/alerts/types.ts | 4 - .../endpoint/endpoint_app_context_services.ts | 17 +- .../endpoint/lib/policy/license_watch.test.ts | 2 +- .../server/endpoint/mocks.ts | 5 +- .../endpoint/routes/actions/isolation.test.ts | 342 ++++++++++++++++++ .../endpoint/routes/actions/isolation.ts | 180 ++++++--- .../routes/metadata/query_builders.ts | 26 ++ .../server/endpoint/services/index.ts | 1 + .../server/endpoint/services/lookup_agent.ts | 30 ++ .../fleet_integration.test.ts | 2 +- .../fleet_integration/fleet_integration.ts | 8 +- .../handlers/install_prepackaged_rules.ts | 8 +- .../validate_policy_against_license.ts | 2 +- .../lib/license/{license.ts => index.ts} | 2 +- .../server/lib/timeline/utils/common.ts | 4 +- .../security_solution/server/plugin.ts | 9 +- 24 files changed, 633 insertions(+), 92 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/schema/actions.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/types/actions.ts create mode 100644 x-pack/plugins/security_solution/common/license/index.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/lookup_agent.ts rename x-pack/plugins/security_solution/server/lib/license/{license.ts => index.ts} (82%) diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 02299a5398555..a5718af1d42c5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -27,4 +27,5 @@ export const BASE_POLICY_ROUTE = `/api/endpoint/policy`; export const AGENT_POLICY_SUMMARY_ROUTE = `${BASE_POLICY_ROUTE}/summaries`; /** Host Isolation Routes */ -export const HOST_ISOLATION_CREATE_API = `/api/endpoint/isolate`; +export const ISOLATE_HOST_ROUTE = `/api/endpoint/isolate`; +export const UNISOLATE_HOST_ROUTE = `/api/endpoint/unisolate`; diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 36d0b0cbf3b21..4a86d7fd4de77 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -254,6 +254,12 @@ interface HostInfo { version: number; }; }; + configuration: { + isolation: boolean; + }; + state: { + isolation: boolean; + }; }; } @@ -458,6 +464,12 @@ export class EndpointDocGenerator extends BaseDataGenerator { policy: { applied: this.randomChoice(APPLIED_POLICIES), }, + configuration: { + isolation: false, + }, + state: { + isolation: false, + }, }, }; } diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts new file mode 100644 index 0000000000000..8f9a5abe2918b --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +export const HostIsolationRequestSchema = { + body: schema.object({ + agent_ids: schema.nullable(schema.arrayOf(schema.string())), + endpoint_ids: schema.nullable(schema.arrayOf(schema.string())), + alert_ids: schema.nullable(schema.arrayOf(schema.string())), + case_ids: schema.nullable(schema.arrayOf(schema.string())), + comment: schema.nullable(schema.string()), + }), +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts new file mode 100644 index 0000000000000..a14d26c16aaaf --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type ISOLATION_ACTIONS = 'isolate' | 'unisolate'; + +export interface EndpointAction { + action_id: string; + '@timestamp': string; + expiration: string; + type: 'INPUT_ACTION'; + input_type: 'endpoint'; + agents: string[]; + user_id: string; + data: { + command: ISOLATION_ACTIONS; + comment?: string; + }; +} + +export interface HostIsolationResponse { + action?: string; + message?: string; +} diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index bed9c2880440a..c58e67b5d4fd4 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -9,6 +9,7 @@ import { ApplicationStart } from 'kibana/public'; import { NewPackagePolicy, PackagePolicy } from '../../../../fleet/common'; import { ManifestSchema } from '../schema/manifest'; +export * from './actions'; export * from './os'; export * from './trusted_apps'; @@ -466,6 +467,12 @@ export type HostMetadata = Immutable<{ version: number; }; }; + configuration: { + isolation?: boolean; + }; + state: { + isolation?: boolean; + }; }; agent: { id: string; diff --git a/x-pack/plugins/security_solution/common/license/index.ts b/x-pack/plugins/security_solution/common/license/index.ts new file mode 100644 index 0000000000000..50532517edee8 --- /dev/null +++ b/x-pack/plugins/security_solution/common/license/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { LicenseService } from './license'; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_license.ts b/x-pack/plugins/security_solution/public/common/hooks/use_license.ts index 645e261be2352..425878b98281c 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_license.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_license.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LicenseService } from '../../../common/license/license'; +import { LicenseService } from '../../../common/license'; export const licenseService = new LicenseService(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts index 6c9f1fd16a704..dbcb11383432f 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts @@ -6,13 +6,14 @@ */ import { UpdateDocumentByQueryResponse } from 'elasticsearch'; +import { HostIsolationResponse } from '../../../../../common/endpoint/types'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL, DETECTION_ENGINE_SIGNALS_STATUS_URL, DETECTION_ENGINE_INDEX_URL, DETECTION_ENGINE_PRIVILEGES_URL, } from '../../../../../common/constants'; -import { HOST_ISOLATION_CREATE_API } from '../../../../../common/endpoint/constants'; +import { ISOLATE_HOST_ROUTE } from '../../../../../common/endpoint/constants'; import { KibanaServices } from '../../../../common/lib/kibana'; import { BasicSignals, @@ -21,7 +22,6 @@ import { AlertSearchResponse, AlertsIndex, UpdateAlertStatusProps, - HostIsolationResponse, } from './types'; /** @@ -119,7 +119,7 @@ export const createHostIsolation = async ({ agentId: string; comment?: string; }): Promise => - KibanaServices.get().http.fetch(HOST_ISOLATION_CREATE_API, { + KibanaServices.get().http.fetch(ISOLATE_HOST_ROUTE, { method: 'POST', body: JSON.stringify({ agent_ids: [agentId], diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts index 4aad6da29dfe8..26108ca939a57 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts @@ -48,10 +48,6 @@ export interface AlertsIndex { index_mapping_outdated: boolean; } -export interface HostIsolationResponse { - action: string; -} - export interface Privilege { username: string; has_all_requested: boolean; diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 103e3ae80831a..aebed0723c3b5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -12,7 +12,7 @@ import { SavedObjectsClientContract, } from 'src/core/server'; import { ExceptionListClient } from '../../../lists/server'; -import { SecurityPluginSetup } from '../../../security/server'; +import { SecurityPluginStart } from '../../../security/server'; import { AgentService, FleetStartContract, @@ -36,7 +36,7 @@ import { ElasticsearchAssetType } from '../../../fleet/common/types/models'; import { metadataTransformPrefix } from '../../common/endpoint/constants'; import { AppClientFactory } from '../client'; import { ConfigType } from '../config'; -import { LicenseService } from '../../common/license/license'; +import { LicenseService } from '../../common/license'; import { ExperimentalFeatures, parseExperimentalConfigValue, @@ -91,7 +91,7 @@ export type EndpointAppContextServiceStartContract = Partial< logger: Logger; manifestManager?: ManifestManager; appClientFactory: AppClientFactory; - security: SecurityPluginSetup; + security: SecurityPluginStart; alerting: AlertsPluginStartContract; config: ConfigType; registerIngestCallback?: FleetStartContract['registerExternalCallback']; @@ -112,6 +112,8 @@ export class EndpointAppContextService { private savedObjectsStart: SavedObjectsServiceStart | undefined; private metadataService: MetadataService | undefined; private config: ConfigType | undefined; + private license: LicenseService | undefined; + public security: SecurityPluginStart | undefined; private experimentalFeatures: ExperimentalFeatures | undefined; @@ -123,6 +125,8 @@ export class EndpointAppContextService { this.savedObjectsStart = dependencies.savedObjectsStart; this.metadataService = createMetadataService(dependencies.packageService!); this.config = dependencies.config; + this.license = dependencies.licenseService; + this.security = dependencies.security; this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental); @@ -180,4 +184,11 @@ export class EndpointAppContextService { } return this.savedObjectsStart.getScopedClient(req, { excludedWrappers: ['security'] }); } + + public getLicenseService(): LicenseService { + if (!this.license) { + throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); + } + return this.license; + } } diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts index 8397c6cd1ae71..176318546b209 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts @@ -11,7 +11,7 @@ import { loggingSystemMock, savedObjectsServiceMock, } from 'src/core/server/mocks'; -import { LicenseService } from '../../../../common/license/license'; +import { LicenseService } from '../../../../common/license'; import { createPackagePolicyServiceMock } from '../../../../../fleet/server/mocks'; import { PolicyWatcher } from './license_watch'; import { ILicense } from '../../../../../licensing/common/types'; diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index ba490bf362cc7..40d4b1a877b2b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -28,8 +28,7 @@ import { ManifestManager } from './services/artifacts/manifest_manager/manifest_ import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock'; import { EndpointAppContext } from './types'; import { MetadataRequestContext } from './routes/metadata/handlers'; -// import { licenseMock } from '../../../licensing/common/licensing.mock'; -import { LicenseService } from '../../common/license/license'; +import { LicenseService } from '../../common/license'; import { SecuritySolutionRequestHandlerContext } from '../types'; import { parseExperimentalConfigValue } from '../../common/experimental_features'; @@ -78,7 +77,7 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< savedObjectsStart: savedObjectsServiceMock.createStartContract(), manifestManager: getManifestManagerMock(), appClientFactory: factory, - security: securityMock.createSetup(), + security: securityMock.createStart(), alerting: alertsMock.createStart(), config, licenseService: new LicenseService(), diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts new file mode 100644 index 0000000000000..306f37796c3fd --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts @@ -0,0 +1,342 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { + ILegacyClusterClient, + KibanaResponseFactory, + RequestHandler, + RouteConfig, +} from 'kibana/server'; +import { + elasticsearchServiceMock, + httpServerMock, + httpServiceMock, + loggingSystemMock, + savedObjectsClientMock, +} from 'src/core/server/mocks'; +import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; +import { SecuritySolutionRequestHandlerContext } from '../../../types'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; +import { + createMockEndpointAppContextServiceStartContract, + createMockPackageService, + createRouteHandlerContext, +} from '../../mocks'; +import { registerHostIsolationRoutes } from './isolation'; +import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; +import { LicenseService } from '../../../../common/license'; +import { Subject } from 'rxjs'; +import { ILicense } from '../../../../../licensing/common/types'; +import { licenseMock } from '../../../../../licensing/common/licensing.mock'; +import { License } from '../../../../../licensing/common/license'; +import { + ISOLATE_HOST_ROUTE, + UNISOLATE_HOST_ROUTE, + metadataTransformPrefix, +} from '../../../../common/endpoint/constants'; +import { + EndpointAction, + HostIsolationResponse, + HostMetadata, +} from '../../../../common/endpoint/types'; +import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { createV2SearchResponse } from '../metadata/support/test_support'; +import { ElasticsearchAssetType } from '../../../../../fleet/common'; + +interface CallRouteInterface { + body?: any; + idxResponse?: any; + searchResponse?: HostMetadata; + mockUser?: any; + license?: License; +} + +const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); +const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } }); + +describe('Host Isolation', () => { + let endpointAppContextService: EndpointAppContextService; + let mockResponse: jest.Mocked; + let licenseService: LicenseService; + let licenseEmitter: Subject; + + let callRoute: ( + routePrefix: string, + opts: CallRouteInterface + ) => Promise>; + const superUser = { + username: 'superuser', + roles: ['superuser'], + }; + + const docGen = new EndpointDocGenerator(); + + beforeEach(() => { + // instantiate... everything + const mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + const mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient() as jest.Mocked; + mockClusterClient.asScoped.mockReturnValue(mockScopedClient); + const routerMock = httpServiceMock.createRouter(); + mockResponse = httpServerMock.createResponseFactory(); + const startContract = createMockEndpointAppContextServiceStartContract(); + endpointAppContextService = new EndpointAppContextService(); + const mockSavedObjectClient = savedObjectsClientMock.create(); + const mockPackageService = createMockPackageService(); + mockPackageService.getInstalledEsAssetReferences.mockReturnValue( + Promise.resolve([ + { + id: 'logs-endpoint.events.security', + type: ElasticsearchAssetType.indexTemplate, + }, + { + id: `${metadataTransformPrefix}-0.16.0-dev.0`, + type: ElasticsearchAssetType.transform, + }, + ]) + ); + endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); + licenseEmitter = new Subject(); + licenseService = new LicenseService(); + licenseService.start(licenseEmitter); + endpointAppContextService.start({ + ...startContract, + licenseService, + packageService: mockPackageService, + }); + + // add the host isolation route handlers to routerMock + registerHostIsolationRoutes(routerMock, { + logFactory: loggingSystemMock.create(), + service: endpointAppContextService, + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); + + // define a convenience function to execute an API call for a given route, body, and mocked response from ES + // it returns the requestContext mock used in the call, to assert internal calls (e.g. the indexed document) + callRoute = async ( + routePrefix: string, + { body, idxResponse, searchResponse, mockUser, license }: CallRouteInterface + ): Promise> => { + const asUser = mockUser ? mockUser : superUser; + (startContract.security.authc.getCurrentUser as jest.Mock).mockImplementationOnce( + () => asUser + ); + const ctx = createRouteHandlerContext(mockScopedClient, mockSavedObjectClient); + const withIdxResp = idxResponse ? idxResponse : { statusCode: 201 }; + ctx.core.elasticsearch.client.asCurrentUser.index = jest + .fn() + .mockImplementationOnce(() => Promise.resolve(withIdxResp)); + ctx.core.elasticsearch.client.asCurrentUser.search = jest + .fn() + .mockImplementationOnce(() => + Promise.resolve({ body: createV2SearchResponse(searchResponse) }) + ); + const withLicense = license ? license : Platinum; + licenseEmitter.next(withLicense); + const mockRequest = httpServerMock.createKibanaRequest({ body }); + const [, routeHandler]: [ + RouteConfig, + RequestHandler + ] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(routePrefix))!; + await routeHandler(ctx, mockRequest, mockResponse); + return (ctx as unknown) as jest.Mocked; + }; + }); + + afterEach(() => { + endpointAppContextService.stop(); + licenseService.stop(); + licenseEmitter.complete(); + }); + + it('errors if no endpoint or agent is provided', async () => { + await callRoute(ISOLATE_HOST_ROUTE, {}); + expect(mockResponse.badRequest).toBeCalled(); + }); + it('succeeds when an agent ID is provided', async () => { + await callRoute(ISOLATE_HOST_ROUTE, { body: { agent_ids: ['XYZ'] } }); + expect(mockResponse.ok).toBeCalled(); + }); + it('reports elasticsearch errors creating an action', async () => { + const ErrMessage = 'something went wrong?'; + + await callRoute(ISOLATE_HOST_ROUTE, { + body: { agent_ids: ['XYZ'] }, + idxResponse: { + statusCode: 500, + body: { + result: ErrMessage, + }, + }, + }); + expect(mockResponse.ok).not.toBeCalled(); + const response = mockResponse.customError.mock.calls[0][0]; + expect(response.statusCode).toEqual(500); + expect((response.body as HostIsolationResponse).message).toEqual(ErrMessage); + }); + it('accepts a comment field', async () => { + await callRoute(ISOLATE_HOST_ROUTE, { body: { agent_ids: ['XYZ'], comment: 'XYZ' } }); + expect(mockResponse.ok).toBeCalled(); + }); + it('sends the action to the requested agent', async () => { + const AgentID = '123-ABC'; + const ctx = await callRoute(ISOLATE_HOST_ROUTE, { + body: { agent_ids: [AgentID] }, + }); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + expect(actionDoc.agents).toContain(AgentID); + }); + it('records the user who performed the action to the action record', async () => { + const testU = { username: 'testuser', roles: ['superuser'] }; + const ctx = await callRoute(ISOLATE_HOST_ROUTE, { + body: { agent_ids: ['XYZ'] }, + mockUser: testU, + }); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + expect(actionDoc.user_id).toEqual(testU.username); + }); + it('records the comment in the action payload', async () => { + const CommentText = "I am isolating this because it's Friday"; + const ctx = await callRoute(ISOLATE_HOST_ROUTE, { + body: { agent_ids: ['XYZ'], comment: CommentText }, + }); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + expect(actionDoc.data.comment).toEqual(CommentText); + }); + it('creates an action and returns its ID', async () => { + const ctx = await callRoute(ISOLATE_HOST_ROUTE, { + body: { agent_ids: ['XYZ'], comment: 'XYZ' }, + }); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + const actionID = actionDoc.action_id; + expect(mockResponse.ok).toBeCalled(); + expect((mockResponse.ok.mock.calls[0][0]?.body as HostIsolationResponse).action).toEqual( + actionID + ); + }); + + it('succeeds when just an endpoint ID is provided', async () => { + await callRoute(ISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] } }); + expect(mockResponse.ok).toBeCalled(); + }); + it('sends the action to the correct agent when endpoint ID is given', async () => { + const doc = docGen.generateHostMetadata(); + const AgentID = doc.elastic.agent.id; + + const ctx = await callRoute(ISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'] }, + searchResponse: doc, + }); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + expect(actionDoc.agents).toContain(AgentID); + }); + it('combines given agent IDs and endpoint IDs', async () => { + const doc = docGen.generateHostMetadata(); + const explicitAgentID = 'XYZ'; + const lookupAgentID = doc.elastic.agent.id; + + const ctx = await callRoute(ISOLATE_HOST_ROUTE, { + body: { agent_ids: [explicitAgentID], endpoint_ids: ['XYZ'] }, + searchResponse: doc, + }); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + expect(actionDoc.agents).toContain(explicitAgentID); + expect(actionDoc.agents).toContain(lookupAgentID); + }); + + it('sends the isolate command payload from the isolate route', async () => { + const ctx = await callRoute(ISOLATE_HOST_ROUTE, { + body: { agent_ids: ['XYZ'] }, + }); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + expect(actionDoc.data.command).toEqual('isolate'); + }); + it('sends the unisolate command payload from the unisolate route', async () => { + const ctx = await callRoute(UNISOLATE_HOST_ROUTE, { + body: { agent_ids: ['XYZ'] }, + }); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + expect(actionDoc.data.command).toEqual('unisolate'); + }); + + describe('License Level', () => { + it('allows platinum license levels to isolate hosts', async () => { + await callRoute(ISOLATE_HOST_ROUTE, { + body: { agent_ids: ['XYZ'] }, + license: Platinum, + }); + expect(mockResponse.ok).toBeCalled(); + }); + it('prohibits license levels less than platinum from isolating hosts', async () => { + licenseEmitter.next(Gold); + await callRoute(ISOLATE_HOST_ROUTE, { + body: { agent_ids: ['XYZ'] }, + license: Gold, + }); + expect(mockResponse.forbidden).toBeCalled(); + }); + it('allows any license level to unisolate', async () => { + licenseEmitter.next(Gold); + await callRoute(UNISOLATE_HOST_ROUTE, { + body: { agent_ids: ['XYZ'] }, + license: Gold, + }); + expect(mockResponse.ok).toBeCalled(); + }); + }); + + describe('User Level', () => { + it('allows superuser to perform isolation', async () => { + const superU = { username: 'foo', roles: ['superuser'] }; + await callRoute(ISOLATE_HOST_ROUTE, { + body: { agent_ids: ['XYZ'] }, + mockUser: superU, + }); + expect(mockResponse.ok).toBeCalled(); + }); + it('allows superuser to perform unisolation', async () => { + const superU = { username: 'foo', roles: ['superuser'] }; + await callRoute(UNISOLATE_HOST_ROUTE, { + body: { agent_ids: ['XYZ'] }, + mockUser: superU, + }); + expect(mockResponse.ok).toBeCalled(); + }); + + it('prohibits non-admin user from performing isolation', async () => { + const superU = { username: 'foo', roles: ['user'] }; + await callRoute(ISOLATE_HOST_ROUTE, { + body: { agent_ids: ['XYZ'] }, + mockUser: superU, + }); + expect(mockResponse.forbidden).toBeCalled(); + }); + it('prohibits non-admin user from performing unisolation', async () => { + const superU = { username: 'foo', roles: ['user'] }; + await callRoute(UNISOLATE_HOST_ROUTE, { + body: { agent_ids: ['XYZ'] }, + mockUser: superU, + }); + expect(mockResponse.forbidden).toBeCalled(); + }); + }); + + describe('Cases', () => { + it.todo('logs a comment to the provided case'); + it.todo('logs a comment to any cases associated with the given alert'); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index d34945d92b6e1..2471eef2bc14d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -5,81 +5,145 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; - -import { IRouter } from 'src/core/server'; +import moment from 'moment'; +import { RequestHandler } from 'src/core/server'; +import uuid from 'uuid'; +import { TypeOf } from '@kbn/config-schema'; +import { HostIsolationRequestSchema } from '../../../../common/endpoint/schema/actions'; +import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE } from '../../../../common/endpoint/constants'; +import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common'; +import { EndpointAction } from '../../../../common/endpoint/types'; +import { + SecuritySolutionPluginRouter, + SecuritySolutionRequestHandlerContext, +} from '../../../types'; +import { getAgentIDsForEndpoints } from '../../services'; import { EndpointAppContext } from '../../types'; +export const userCanIsolate = (roles: readonly string[] | undefined): boolean => { + // only superusers can write to the fleet index (or look up endpoint data to convert endp ID to agent ID) + if (!roles || roles.length === 0) { + return false; + } + return roles.includes('superuser'); +}; + /** * Registers the Host-(un-)isolation routes */ -export function registerHostIsolationRoutes(router: IRouter, endpointContext: EndpointAppContext) { +export function registerHostIsolationRoutes( + router: SecuritySolutionPluginRouter, + endpointContext: EndpointAppContext +) { // perform isolation router.post( { - path: `/api/endpoint/isolate`, - validate: { - body: schema.object({ - agent_ids: schema.nullable(schema.arrayOf(schema.string())), - endpoint_ids: schema.nullable(schema.arrayOf(schema.string())), - alert_ids: schema.nullable(schema.arrayOf(schema.string())), - case_ids: schema.nullable(schema.arrayOf(schema.string())), - comment: schema.nullable(schema.string()), - }), - }, - options: { authRequired: true }, + path: ISOLATE_HOST_ROUTE, + validate: HostIsolationRequestSchema, + options: { authRequired: true, tags: ['access:securitySolution'] }, }, - async (context, req, res) => { - if ( - (req.body.agent_ids === null || req.body.agent_ids.length === 0) && - (req.body.endpoint_ids === null || req.body.endpoint_ids.length === 0) - ) { - return res.badRequest({ - body: { - message: 'At least one agent ID or endpoint ID is required', - }, - }); - } - - return res.ok({ - body: { - action: '713085d6-ab45-4e9e-b41d-96563cafdd97', - }, - }); - } + isolationRequestHandler(endpointContext, true) ); // perform UN-isolate router.post( { - path: `/api/endpoint/unisolate`, - validate: { - body: schema.object({ - agent_ids: schema.nullable(schema.arrayOf(schema.string())), - endpoint_ids: schema.nullable(schema.arrayOf(schema.string())), - alert_ids: schema.nullable(schema.arrayOf(schema.string())), - case_ids: schema.nullable(schema.arrayOf(schema.string())), - comment: schema.nullable(schema.string()), - }), - }, - options: { authRequired: true }, + path: UNISOLATE_HOST_ROUTE, + validate: HostIsolationRequestSchema, + options: { authRequired: true, tags: ['access:securitySolution'] }, }, - async (context, req, res) => { - if ( - (req.body.agent_ids === null || req.body.agent_ids.length === 0) && - (req.body.endpoint_ids === null || req.body.endpoint_ids.length === 0) - ) { - return res.badRequest({ - body: { - message: 'At least one agent ID or endpoint ID is required', + isolationRequestHandler(endpointContext, false) + ); +} + +export const isolationRequestHandler = function ( + endpointContext: EndpointAppContext, + isolate: boolean +): RequestHandler< + unknown, + unknown, + TypeOf, + SecuritySolutionRequestHandlerContext +> { + return async (context, req, res) => { + if ( + (!req.body.agent_ids || req.body.agent_ids.length === 0) && + (!req.body.endpoint_ids || req.body.endpoint_ids.length === 0) + ) { + return res.badRequest({ + body: { + message: 'At least one agent ID or endpoint ID is required', + }, + }); + } + + // only allow admin users + const user = endpointContext.service.security?.authc.getCurrentUser(req); + if (!userCanIsolate(user?.roles)) { + return res.forbidden({ + body: { + message: 'You do not have permission to perform this action', + }, + }); + } + + // isolation requires plat+ + if (isolate && !endpointContext.service.getLicenseService()?.isPlatinumPlus()) { + return res.forbidden({ + body: { + message: 'Your license level does not allow for this action', + }, + }); + } + + // translate any endpoint_ids into agent_ids + let agentIDs = req.body.agent_ids?.slice() || []; + if (req.body.endpoint_ids && req.body.endpoint_ids.length > 0) { + const newIDs = await getAgentIDsForEndpoints(req.body.endpoint_ids, context, endpointContext); + agentIDs = agentIDs.concat(newIDs); + } + agentIDs = [...new Set(agentIDs)]; // dedupe + + // create an Action ID and dispatch it to ES & Fleet Server + const esClient = context.core.elasticsearch.client.asCurrentUser; + const actionID = uuid.v4(); + let result; + try { + result = await esClient.index({ + index: AGENT_ACTIONS_INDEX, + body: { + action_id: actionID, + '@timestamp': moment().toISOString(), + expiration: moment().add(2, 'weeks').toISOString(), + type: 'INPUT_ACTION', + input_type: 'endpoint', + agents: agentIDs, + user_id: user?.username, + data: { + command: isolate ? 'isolate' : 'unisolate', + comment: req.body.comment, }, - }); - } - return res.ok({ + } as EndpointAction, + }); + } catch (e) { + return res.customError({ + statusCode: 500, + body: { message: e }, + }); + } + + if (result.statusCode !== 201) { + return res.customError({ + statusCode: 500, body: { - action: '53ba1dd1-58a7-407e-b2a9-6843d9980068', + message: result.body.result, }, }); } - ); -} + return res.ok({ + body: { + action: actionID, + }, + }); + }; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index 51aa6acf26d72..a5259dd44cf2b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -169,3 +169,29 @@ export function getESQueryHostMetadataByID( index: metadataQueryStrategy.index, }; } + +export function getESQueryHostMetadataByIDs( + agentIDs: string[], + metadataQueryStrategy: MetadataQueryStrategy +) { + return { + body: { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { terms: { 'agent.id': agentIDs } }, + { terms: { 'HostDetails.agent.id': agentIDs } }, + ], + }, + }, + ], + }, + }, + sort: MetadataSortMethod, + }, + index: metadataQueryStrategy.index, + }; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/services/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/index.ts index b77e925a4ffb2..9fabd043e2950 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/index.ts @@ -6,3 +6,4 @@ */ export * from './artifacts'; +export { getAgentIDsForEndpoints } from './lookup_agent'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/lookup_agent.ts b/x-pack/plugins/security_solution/server/endpoint/services/lookup_agent.ts new file mode 100644 index 0000000000000..e82b548641290 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/lookup_agent.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SearchRequest } from '@elastic/elasticsearch/api/types'; +import { SearchResponse } from 'elasticsearch'; +import { HostMetadata } from '../../../common/endpoint/types'; +import { SecuritySolutionRequestHandlerContext } from '../../types'; +import { getESQueryHostMetadataByIDs } from '../routes/metadata/query_builders'; +import { EndpointAppContext } from '../types'; + +export async function getAgentIDsForEndpoints( + endpointIDs: string[], + requestHandlerContext: SecuritySolutionRequestHandlerContext, + endpointAppContext: EndpointAppContext +): Promise { + const queryStrategy = await endpointAppContext.service + ?.getMetadataService() + ?.queryStrategy(requestHandlerContext.core.savedObjects.client); + + const query = getESQueryHostMetadataByIDs(endpointIDs, queryStrategy!); + const esClient = requestHandlerContext.core.elasticsearch.client.asCurrentUser; + const { body } = await esClient.search(query as SearchRequest); + const hosts = queryStrategy!.queryResponseToHostListResult(body as SearchResponse); + + return hosts.resultList.map((x: HostMetadata): string => x.elastic.agent.id); +} diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 1c016ec0cc799..c0dc3a9343a7d 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -21,7 +21,7 @@ import { createMockConfig, requestContextMock } from '../lib/detection_engine/ro import { EndpointAppContextServiceStartContract } from '../endpoint/endpoint_app_context_services'; import { createMockEndpointAppContextServiceStartContract } from '../endpoint/mocks'; import { licenseMock } from '../../../licensing/common/licensing.mock'; -import { LicenseService } from '../../common/license/license'; +import { LicenseService } from '../../common/license'; import { Subject } from 'rxjs'; import { ILicense } from '../../../licensing/common/types'; import { EndpointDocGenerator } from '../../common/endpoint/generate_data'; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index c9939a163c977..9e1bb2f9b32b0 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -8,13 +8,13 @@ import { KibanaRequest, Logger, RequestHandlerContext } from 'kibana/server'; import { ExceptionListClient } from '../../../lists/server'; import { PluginStartContract as AlertsStartContract } from '../../../alerting/server'; -import { SecurityPluginSetup } from '../../../security/server'; +import { SecurityPluginStart } from '../../../security/server'; import { ExternalCallback } from '../../../fleet/server'; import { NewPackagePolicy, UpdatePackagePolicy } from '../../../fleet/common'; import { NewPolicyData, PolicyConfig } from '../../common/endpoint/types'; import { ManifestManager } from '../endpoint/services'; import { AppClientFactory } from '../client'; -import { LicenseService } from '../../common/license/license'; +import { LicenseService } from '../../common/license'; import { installPrepackagedRules } from './handlers/install_prepackaged_rules'; import { createPolicyArtifactManifest } from './handlers/create_policy_artifact_manifest'; import { createDefaultPolicy } from './handlers/create_default_policy'; @@ -34,7 +34,7 @@ export const getPackagePolicyCreateCallback = ( manifestManager: ManifestManager, appClientFactory: AppClientFactory, maxTimelineImportExportSize: number, - securitySetup: SecurityPluginSetup, + securityStart: SecurityPluginStart, alerts: AlertsStartContract, licenseService: LicenseService, exceptionsClient: ExceptionListClient | undefined @@ -58,7 +58,7 @@ export const getPackagePolicyCreateCallback = ( appClientFactory, context, request, - securitySetup, + securityStart, alerts, maxTimelineImportExportSize, exceptionsClient, diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts index 944707d2afb28..a387b7e3fdca5 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts @@ -8,7 +8,7 @@ import { KibanaRequest, Logger, RequestHandlerContext } from 'kibana/server'; import { ExceptionListClient } from '../../../../lists/server'; import { PluginStartContract as AlertsStartContract } from '../../../../alerting/server'; -import { SecurityPluginSetup } from '../../../../security/server'; +import { SecurityPluginStart } from '../../../../security/server'; import { AppClientFactory } from '../../client'; import { createDetectionIndex } from '../../lib/detection_engine/routes/index/create_index_route'; import { createPrepackagedRules } from '../../lib/detection_engine/routes/rules/add_prepackaged_rules_route'; @@ -19,7 +19,7 @@ export interface InstallPrepackagedRulesProps { appClientFactory: AppClientFactory; context: RequestHandlerContext; request: KibanaRequest; - securitySetup: SecurityPluginSetup; + securityStart: SecurityPluginStart; alerts: AlertsStartContract; maxTimelineImportExportSize: number; exceptionsClient: ExceptionListClient; @@ -34,7 +34,7 @@ export const installPrepackagedRules = async ({ appClientFactory, context, request, - securitySetup, + securityStart, alerts, maxTimelineImportExportSize, exceptionsClient, @@ -46,7 +46,7 @@ export const installPrepackagedRules = async ({ // It doesn't have access to SecuritySolutionRequestHandlerContext in runtime. // Muting the error to have green CI. // @ts-expect-error - const frameworkRequest = await buildFrameworkRequest(context, securitySetup, request); + const frameworkRequest = await buildFrameworkRequest(context, securityStart, request); // Create detection index & rules (if necessary). move past any failure, this is just a convenience try { diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_policy_against_license.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_policy_against_license.ts index 50dff2ac89f49..a82f89e31803f 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_policy_against_license.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_policy_against_license.ts @@ -8,7 +8,7 @@ import { Logger } from 'kibana/server'; import { isEndpointPolicyValidForLicense } from '../../../common/license/policy_config'; import { PolicyConfig } from '../../../common/endpoint/types'; -import { LicenseService } from '../../../common/license/license'; +import { LicenseService } from '../../../common/license'; export const validatePolicyAgainstLicense = ( policyConfig: PolicyConfig, diff --git a/x-pack/plugins/security_solution/server/lib/license/license.ts b/x-pack/plugins/security_solution/server/lib/license/index.ts similarity index 82% rename from x-pack/plugins/security_solution/server/lib/license/license.ts rename to x-pack/plugins/security_solution/server/lib/license/index.ts index 697e0b1ac166f..a1c43f80d32d1 100644 --- a/x-pack/plugins/security_solution/server/lib/license/license.ts +++ b/x-pack/plugins/security_solution/server/lib/license/index.ts @@ -5,6 +5,6 @@ * 2.0. */ -import { LicenseService } from '../../../common/license/license'; +import { LicenseService } from '../../../common/license'; export const licenseService = new LicenseService(); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts index f457a1a11422c..c4ddefd925b37 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts @@ -14,14 +14,14 @@ import { schema } from '@kbn/config-schema'; import { isObject } from 'lodash/fp'; import { KibanaRequest } from 'src/core/server'; -import { SetupPlugins } from '../../../plugin'; +import { SetupPlugins, StartPlugins } from '../../../plugin'; import type { SecuritySolutionRequestHandlerContext } from '../../../types'; import { FrameworkRequest } from '../../framework'; export const buildFrameworkRequest = async ( context: SecuritySolutionRequestHandlerContext, - security: SetupPlugins['security'], + security: StartPlugins['security'] | SetupPlugins['security'] | undefined, request: KibanaRequest ): Promise => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 3497041e776e5..46467a21ca7ab 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -27,7 +27,7 @@ import { PluginSetupContract as AlertingSetup, PluginStartContract as AlertPluginStartContract, } from '../../alerting/server'; -import { SecurityPluginSetup as SecuritySetup } from '../../security/server'; +import { SecurityPluginSetup as SecuritySetup, SecurityPluginStart } from '../../security/server'; import { PluginSetupContract as FeaturesSetup } from '../../features/server'; import { MlPluginSetup as MlSetup } from '../../ml/server'; import { ListPluginSetup } from '../../lists/server'; @@ -73,7 +73,7 @@ import { TelemetryPluginStart, TelemetryPluginSetup, } from '../../../../src/plugins/telemetry/server'; -import { licenseService } from './lib/license/license'; +import { licenseService } from './lib/license'; import { PolicyWatcher } from './endpoint/lib/policy/license_watch'; import { securitySolutionTimelineEqlSearchStrategyProvider } from './search_strategy/timeline/eql'; import { parseExperimentalConfigValue } from '../common/experimental_features'; @@ -100,6 +100,7 @@ export interface StartPlugins { licensing: LicensingPluginStart; taskManager?: TaskManagerStartContract; telemetry?: TelemetryPluginStart; + security: SecurityPluginStart; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -132,7 +133,6 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { this.logger.debug('plugin setup'); - this.setupPlugins = plugins; const config = this.config; const globalConfig = this.context.config.legacy.get(); @@ -397,7 +396,7 @@ export class Plugin implements IPlugin Date: Mon, 10 May 2021 19:44:01 +0200 Subject: [PATCH 17/69] [Osquery] Fix Osquery plugin initialization (#99591) --- x-pack/plugins/osquery/public/plugin.ts | 33 ++++++++++++------- .../scheduled_query_groups/form/index.tsx | 5 +++ .../factory/results/query.all_results.dsl.ts | 2 +- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/osquery/public/plugin.ts b/x-pack/plugins/osquery/public/plugin.ts index c0a097cb3ba28..d1e7154fb0dc0 100644 --- a/x-pack/plugins/osquery/public/plugin.ts +++ b/x-pack/plugins/osquery/public/plugin.ts @@ -38,6 +38,14 @@ export function toggleOsqueryPlugin( http: CoreStart['http'], registerExtension?: StartPlugins['fleet']['registerExtension'] ) { + if (http.anonymousPaths.isAnonymous(window.location.pathname)) { + updater$.next(() => ({ + status: AppStatus.inaccessible, + navLinkStatus: AppNavLinkStatus.hidden, + })); + return; + } + http .fetch(epmRouteService.getListPath(), { query: { experimental: true } }) .then(({ response }) => { @@ -134,22 +142,23 @@ export class OsqueryPlugin implements Plugin ({ status: AppStatus.inaccessible, + navLinkStatus: AppNavLinkStatus.hidden, })); } diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx index 68652e13bed07..8924a61d181b6 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx @@ -151,6 +151,11 @@ const ScheduledQueryGroupFormComponent: React.FC = // @ts-expect-error update types draft.inputs[0].streams.forEach((stream) => { delete stream.compiled_stream; + + // we don't want to send id as null when creating the policy + if (stream.id == null) { + delete stream.id; + } }); return draft; }); diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts index 6ef00b0ea3058..b560fd3c364e9 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts @@ -43,7 +43,7 @@ export const buildResultsQuery = ({ aggs: { count_by_agent_id: { terms: { - field: 'agent.id', + field: 'elastic_agent.id', size: 10000, }, }, From baf11e874968eec80bce94aaabd5f555a1cbb883 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Mon, 10 May 2021 14:11:23 -0400 Subject: [PATCH 18/69] [Fleet] Remove timestamp field from component template (#99619) * Remove timestamp field from component template Elasticsearch bug fixed upstream allows us to remove this field from the template: elastic/elasticsearch/#58956 Closes #71095 * Remove timestamp field from functional test --- .../services/epm/elasticsearch/template/install.ts | 9 --------- .../fleet_api_integration/apis/epm/install_overrides.ts | 6 ------ .../test/fleet_api_integration/apis/epm/update_assets.ts | 1 - 3 files changed, 16 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 98ba970fda39b..9e8277eb6171f 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -198,15 +198,6 @@ function buildComponentTemplates(registryElasticsearch: RegistryElasticsearch | template: { mappings: { ...registryElasticsearch['index_template.mappings'], - // temporary change until https://github.com/elastic/elasticsearch/issues/58956 is resolved - // hopefully we'll be able to remove the entire properties section once that issue is resolved - properties: { - // if the timestamp_field changes here: https://github.com/elastic/kibana/blob/master/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts#L309 - // we'll need to update this as well - '@timestamp': { - type: 'date', - }, - }, }, }, }; diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts b/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts index 18a7bd59cdd6c..1b916dff573af 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts @@ -62,12 +62,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.component_templates[0].component_template.template.mappings.dynamic).to.be( false ); - // Make sure that the `@timestamp` field exists and is set to date - // this can be removed once https://github.com/elastic/elasticsearch/issues/58956 is resolved - expect( - body.component_templates[0].component_template.template.mappings.properties['@timestamp'] - .type - ).to.be('date'); ({ body } = await es.transport.request({ method: 'GET', diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts index 9b55822311bd7..a6f79414ab8c0 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts @@ -207,7 +207,6 @@ export default function (providerContext: FtrProviderContext) { expect(res.statusCode).equal(200); expect(res.body.component_templates[0].component_template.template.mappings).eql({ dynamic: true, - properties: { '@timestamp': { type: 'date' } }, }); const resSettings = await es.transport.request({ method: 'GET', From e9e7314c3b23c4cf2574af907fb9464e6d28eec4 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Mon, 10 May 2021 13:18:40 -0500 Subject: [PATCH 19/69] [ML] Make swimlane annotation markers look less like a 0 (#99592) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../explorer/swimlane_annotation_container.tsx | 10 +++------- .../components/timeseries_chart/timeseries_chart.js | 2 -- .../timeseries_chart/timeseries_chart_annotations.ts | 7 +------ 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_annotation_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_annotation_container.tsx index f106aed84aa79..859e18c29f455 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_annotation_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_annotation_container.tsx @@ -17,9 +17,7 @@ export const Y_AXIS_LABEL_WIDTH = 170; export const Y_AXIS_LABEL_PADDING = 8; export const Y_AXIS_LABEL_FONT_COLOR = '#6a717d'; const ANNOTATION_CONTAINER_HEIGHT = 12; -const ANNOTATION_MARGIN = 2; -const ANNOTATION_MIN_WIDTH = 5; -const ANNOTATION_HEIGHT = ANNOTATION_CONTAINER_HEIGHT - 2 * ANNOTATION_MARGIN; +const ANNOTATION_MIN_WIDTH = 8; interface SwimlaneAnnotationContainerProps { chartWidth: number; @@ -93,11 +91,9 @@ export const SwimlaneAnnotationContainer: FC = .append('rect') .classed('mlAnnotationRect', true) .attr('x', d.timestamp >= domain.min ? xScale(d.timestamp) : startingXPos) - .attr('y', ANNOTATION_MARGIN) - .attr('height', ANNOTATION_HEIGHT) + .attr('y', 0) + .attr('height', ANNOTATION_CONTAINER_HEIGHT) .attr('width', Math.max(annotationWidth, ANNOTATION_MIN_WIDTH)) - .attr('rx', ANNOTATION_MARGIN) - .attr('ry', ANNOTATION_MARGIN) .on('mouseover', function () { const startingTime = formatHumanReadableDateTimeSeconds(d.timestamp); const endingTime = diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 23fe648a67598..73c5f58fb80db 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -1108,8 +1108,6 @@ class TimeseriesChartIntl extends Component { ctxAnnotationRects .enter() .append('rect') - .attr('rx', ctxAnnotationMargin) - .attr('ry', ctxAnnotationMargin) .on('mouseover', function (d) { showFocusChartTooltip(d, this); }) diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts index 7a44a0ccdec4d..ac5d15f1695d4 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts @@ -92,8 +92,7 @@ const ANNOTATION_DEFAULT_LEVEL = 1; const ANNOTATION_LEVEL_HEIGHT = 28; const ANNOTATION_UPPER_RECT_MARGIN = 0; const ANNOTATION_UPPER_TEXT_MARGIN = -7; -export const ANNOTATION_MIN_WIDTH = 2; -const ANNOTATION_RECT_BORDER_RADIUS = 2; +export const ANNOTATION_MIN_WIDTH = 8; const ANNOTATION_TEXT_VERTICAL_OFFSET = 26; const ANNOTATION_TEXT_RECT_VERTICAL_OFFSET = 12; const ANNOTATION_TEXT_RECT_WIDTH = 24; @@ -157,8 +156,6 @@ export function renderAnnotations( rects .enter() .append('rect') - .attr('rx', ANNOTATION_RECT_BORDER_RADIUS) - .attr('ry', ANNOTATION_RECT_BORDER_RADIUS) .classed('mlAnnotationRect', true) .attr('mask', `url(#${ANNOTATION_MASK_ID})`) .on('mouseover', onAnnotationMouseOver) @@ -199,8 +196,6 @@ export function renderAnnotations( .classed('mlAnnotationTextRect', true) .attr('width', ANNOTATION_TEXT_RECT_WIDTH) .attr('height', ANNOTATION_TEXT_RECT_HEIGHT) - .attr('rx', ANNOTATION_RECT_BORDER_RADIUS) - .attr('ry', ANNOTATION_RECT_BORDER_RADIUS) .on('mouseover', onAnnotationMouseOver) .on('mouseout', hideFocusChartTooltip) .on('click', onAnnotationClick); From 8e3604fe117275c5d06276612cf6c3edd99460b0 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Mon, 10 May 2021 14:46:01 -0400 Subject: [PATCH 20/69] [Alerting UI] Fixing behavior when trying to render an Index Threshold visualization with invalid data (#99518) * Showing error message not object. Removing error toaster * Updating unit tests * Fixing i18n Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../threshold/visualization.test.tsx | 28 +++++++++++++++++-- .../alert_types/threshold/visualization.tsx | 23 ++++++--------- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.test.tsx index 34239d8a3f890..8434e5ba17dcc 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.test.tsx @@ -163,17 +163,41 @@ describe('ThresholdVisualization', () => { expect(wrapper.find(LineAnnotation)).toHaveLength(1); }); - test('renders error message when getting visualization fails', async () => { + test('renders error callout with message when getting visualization fails', async () => { const errorMessage = 'oh no'; - getThresholdAlertVisualizationData.mockImplementation(() => Promise.reject(errorMessage)); + getThresholdAlertVisualizationData.mockImplementation(() => + Promise.reject(new Error(errorMessage)) + ); const wrapper = await setup(); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="errorCallout"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="errorCallout"]').first().text()).toBe( `Cannot load alert visualization${errorMessage}` ); }); + test('renders error callout even when unable to get message from error', async () => { + getThresholdAlertVisualizationData.mockImplementation(() => + Promise.reject(new Error(undefined)) + ); + const wrapper = await setup(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="errorCallout"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="errorCallout"]').first().text()).toBe( + `Cannot load alert visualization` + ); + }); + test('renders no data message when visualization results are empty', async () => { getThresholdAlertVisualizationData.mockImplementation(() => Promise.resolve({ results: [] })); const wrapper = await setup(); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx index 0d7ed390f20c7..d959bf19c7cd8 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx @@ -7,7 +7,6 @@ import React, { Fragment, useEffect, useState } from 'react'; import { IUiSettingsClient, HttpSetup } from 'kibana/public'; -import { i18n } from '@kbn/i18n'; import { interval } from 'rxjs'; import { AnnotationDomainType, @@ -130,9 +129,10 @@ export const ThresholdVisualization: React.FunctionComponent = ({ groupBy, threshold, } = alertParams; - const { http, notifications, uiSettings } = useKibana().services; + const { http, uiSettings } = useKibana().services; const [loadingState, setLoadingState] = useState(null); - const [error, setError] = useState(undefined); + const [hasError, setHasError] = useState(false); + const [errorMessage, setErrorMessage] = useState(undefined); const [visualizationData, setVisualizationData] = useState>(); const [startVisualizationAt, setStartVisualizationAt] = useState(new Date()); @@ -153,16 +153,11 @@ export const ThresholdVisualization: React.FunctionComponent = ({ setVisualizationData( await getVisualizationData(alertWithoutActions, visualizeOptions, http!) ); + setHasError(false); + setErrorMessage(undefined); } catch (e) { - if (notifications) { - notifications.toasts.addDanger({ - title: i18n.translate( - 'xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage', - { defaultMessage: 'Unable to load visualization' } - ), - }); - } - setError(e); + setHasError(true); + setErrorMessage(e?.body?.message || e?.message); } finally { setLoadingState(LoadingStateType.Idle); } @@ -216,7 +211,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ ); } - if (error) { + if (hasError) { return ( @@ -232,7 +227,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ color="danger" iconType="alert" > - {error} + {errorMessage} ); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c432809355e0f..75f5c8c901807 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22991,7 +22991,6 @@ "xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "アラートビジュアライゼーションを読み込み中…", "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "時間範囲とフィルターが正しいことを確認してください。", "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "このクエリに一致するデータはありません", - "xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage": "ビジュアライゼーションを読み込めません", "xpack.timelines.placeholder": "プラグイン:{name} タイムライン:{timelineId}", "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "ディスティネーションインデックスの削除", "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "ディスティネーションインデックスパターンの削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 46117f739f985..2b562f4cd4410 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23352,7 +23352,6 @@ "xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "正在加载告警可视化……", "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "确认您的时间范围和筛选正确。", "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "没有数据匹配此查询", - "xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage": "无法加载可视化", "xpack.timelines.placeholder": "插件:{name} 时间线:{timelineId}", "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "删除目标索引", "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "删除目标索引模式", From 65a2177dcf1c664011fad4ec7913d9593de0f4cb Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 10 May 2021 20:53:09 +0200 Subject: [PATCH 21/69] cleanup list of allowed circular deps. (#99654) --- src/dev/run_find_plugins_with_circular_deps.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/dev/run_find_plugins_with_circular_deps.ts b/src/dev/run_find_plugins_with_circular_deps.ts index d97dc8e70cd9b..a737bc6a73004 100644 --- a/src/dev/run_find_plugins_with_circular_deps.ts +++ b/src/dev/run_find_plugins_with_circular_deps.ts @@ -20,9 +20,7 @@ interface Options { type CircularDepList = Set; const allowedList: CircularDepList = new Set([ - 'x-pack/plugins/apm -> x-pack/plugins/infra', 'x-pack/plugins/lists -> x-pack/plugins/security_solution', - 'x-pack/plugins/security -> x-pack/plugins/spaces', ]); run( From 5893d67b4b34c5fdc9446d5e63cd908d777c8b3b Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 10 May 2021 20:21:36 +0100 Subject: [PATCH 22/69] [SecuritySolution] Get endpoint metadata (#99452) * getHostEndpoint * add endpointContext * add deps * get endpoint info * clean up * fix tests error * fix types * fix unit tests * fix unit tests * fix unit tests * fix types error * fix types * fix api integration test * fix api integration tests * add comment * review * add getHostInfo * rename getHostInfo into getHostMetaData Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/endpoint/types/index.ts | 5 + .../security_solution/hosts/common/index.ts | 9 + .../ml/criteria/host_to_criteria.ts | 3 + .../hosts/containers/hosts/details/index.tsx | 8 +- .../public/hosts/pages/details/index.tsx | 10 +- .../pages/endpoint_hosts/view/index.tsx | 2 +- .../components/host_overview/index.tsx | 13 +- .../server/endpoint/mocks.ts | 10 +- .../endpoint/routes/metadata/handlers.ts | 163 +++++++++++++----- .../endpoint/routes/metadata/metadata.test.ts | 87 ++++++---- .../routes/metadata/metadata_v1.test.ts | 78 +++++---- .../routes/metadata/query_builders.test.ts | 5 +- .../routes/metadata/query_builders.ts | 7 +- .../routes/metadata/query_builders_v1.test.ts | 3 +- .../metadata/support/query_strategies.ts | 20 +-- .../endpoint/routes/policy/handlers.test.ts | 24 ++- .../server/endpoint/routes/policy/handlers.ts | 2 +- .../endpoint/routes/policy/service.test.ts | 8 +- .../server/endpoint/routes/policy/service.ts | 30 ++-- .../server/endpoint/types.ts | 3 +- .../security_solution/server/plugin.ts | 5 +- .../factory/hosts/authentications/index.tsx | 1 - .../factory/hosts/details/__mocks__/index.ts | 28 +++ .../factory/hosts/details/helpers.ts | 66 ++++++- .../factory/hosts/details/index.test.tsx | 28 ++- .../factory/hosts/details/index.ts | 40 ++++- .../hosts/details/query.host_details.dsl.ts | 19 +- .../security_solution/factory/types.ts | 12 +- .../security_solution/index.ts | 12 +- 29 files changed, 516 insertions(+), 185 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index c58e67b5d4fd4..b9e72bcd625ec 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -414,6 +414,11 @@ export type PolicyInfo = Immutable<{ id: string; }>; +export interface HostMetaDataInfo { + metadata: HostMetadata; + query_strategy_version: MetadataQueryStrategyVersions; +} + export type HostInfo = Immutable<{ metadata: HostMetadata; host_status: HostStatus; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts index a579d8f8d8ef3..3175876a8299c 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts @@ -25,10 +25,16 @@ export interface EndpointFields { endpointPolicy?: Maybe; sensorVersion?: Maybe; policyStatus?: Maybe; + id?: Maybe; +} + +interface AgentFields { + id?: Maybe; } export interface HostItem { _id?: Maybe; + agent?: Maybe; cloud?: Maybe; endpoint?: Maybe; host?: Maybe; @@ -70,6 +76,9 @@ export interface HostAggEsItem { cloud_machine_type?: HostBuckets; cloud_provider?: HostBuckets; cloud_region?: HostBuckets; + endpoint?: { + id: HostBuckets; + }; host_architecture?: HostBuckets; host_id?: HostBuckets; host_ip?: HostBuckets; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.ts b/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.ts index 19eae99757849..ff454da7b1fcd 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.ts @@ -9,6 +9,9 @@ import { HostItem } from '../../../../../common/search_strategy/security_solutio import { CriteriaFields } from '../types'; export const hostToCriteria = (hostItem: HostItem): CriteriaFields[] => { + if (hostItem == null) { + return []; + } if (hostItem.host != null && hostItem.host.name != null) { const criteria: CriteriaFields[] = [ { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx index dd55bdb4c6948..a0f4386be59a4 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx @@ -145,14 +145,14 @@ export const useHostDetails = ({ } return prevRequest; }); - return () => { - searchSubscription$.current.unsubscribe(); - abortCtrl.current.abort(); - }; }, [endDate, hostName, indexNames, startDate]); useEffect(() => { hostDetailsSearch(hostDetailsRequest); + return () => { + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + }; }, [hostDetailsRequest, hostDetailsSearch]); return [loading, hostDetailsResponse]; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 1ff4abb78b210..d88e4f048f917 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -50,6 +50,9 @@ import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { useHostDetails } from '../../containers/hosts/details'; +import { manageQuery } from '../../../common/components/page/manage_query'; + +const HostOverviewManage = manageQuery(HostOverview); const HostDetailsComponent: React.FC = ({ detailName, hostDetailsPagePath }) => { const dispatch = useDispatch(); @@ -93,11 +96,12 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta ); const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); - const [loading, { hostDetails: hostOverview, id }] = useHostDetails({ + const [loading, { hostDetails: hostOverview, id, refetch }] = useHostDetails({ endDate: to, startDate: from, hostName: detailName, indexNames: selectedPatterns, + skip: selectedPatterns.length === 0, }); const filterQuery = convertToBuildEsQuery({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), @@ -141,7 +145,7 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta skip={isInitializing} > {({ isLoadingAnomaliesData, anomaliesData }) => ( - = ({ detailName, hostDeta to: fromTo.to, }); }} + setQuery={setQuery} + refetch={refetch} /> )} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index d28bf6b38fd31..f654efdd89ce1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -321,7 +321,7 @@ export const EndpointList = () => { render: (hostStatus: HostInfo['host_status']) => { return ( diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index c5d51a9466235..fa644d1cbcdac 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -86,14 +86,15 @@ export const HostOverview = React.memo( () => [ { title: i18n.HOST_ID, - description: data.host - ? hostIdRenderer({ host: data.host, noLink: true }) - : getEmptyTagValue(), + description: + data && data.host + ? hostIdRenderer({ host: data.host, noLink: true }) + : getEmptyTagValue(), }, { title: i18n.FIRST_SEEN, description: - data.host != null && data.host.name && data.host.name.length ? ( + data && data.host != null && data.host.name && data.host.name.length ? ( ( { title: i18n.LAST_SEEN, description: - data.host != null && data.host.name && data.host.name.length ? ( + data && data.host != null && data.host.name && data.host.name.length ? ( ( )} - {data.endpoint != null ? ( + {data && data.endpoint != null ? ( <> diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 40d4b1a877b2b..23ea6cc29c3d2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; -import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { loggingSystemMock, savedObjectsServiceMock } from '../../../../../src/core/server/mocks'; +import { IScopedClusterClient, SavedObjectsClientContract } from '../../../../../src/core/server'; import { listMock } from '../../../lists/server/mocks'; import { securityMock } from '../../../security/server/mocks'; import { alertsMock } from '../../../alerting/server/mocks'; @@ -131,11 +131,11 @@ export const createMockMetadataRequestContext = (): jest.Mocked, + dataClient: jest.Mocked, savedObjectsClient: jest.Mocked ) { - const context = xpackMocks.createRequestHandlerContext(); - context.core.elasticsearch.legacy.client = dataClient; + const context = (xpackMocks.createRequestHandlerContext() as unknown) as jest.Mocked; + context.core.elasticsearch.client = dataClient; context.core.savedObjects.client = savedObjectsClient; return context; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 0d59ff2f4ed7b..104383f398646 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -6,11 +6,18 @@ */ import Boom from '@hapi/boom'; -import type { Logger, RequestHandler } from 'kibana/server'; + import { TypeOf } from '@kbn/config-schema'; +import { + IScopedClusterClient, + Logger, + RequestHandler, + SavedObjectsClientContract, +} from '../../../../../../../src/core/server'; import { HostInfo, HostMetadata, + HostMetaDataInfo, HostResultList, HostStatus, MetadataQueryStrategyVersions, @@ -27,9 +34,11 @@ import { findAgentIDsByStatus } from './support/agent_status'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; export interface MetadataRequestContext { + esClient?: IScopedClusterClient; endpointAppContextService: EndpointAppContextService; logger: Logger; - requestHandlerContext: SecuritySolutionRequestHandlerContext; + requestHandlerContext?: SecuritySolutionRequestHandlerContext; + savedObjectsClient?: SavedObjectsClientContract; } const HOST_STATUS_MAPPING = new Map([ @@ -75,9 +84,11 @@ export const getMetadataListRequestHandler = function ( } const metadataRequestContext: MetadataRequestContext = { + esClient: context.core.elasticsearch.client, endpointAppContextService: endpointAppContext.service, logger, requestHandlerContext: context, + savedObjectsClient: context.core.savedObjects.client, }; const unenrolledAgentIds = await findAllUnenrolledAgentIds( @@ -110,9 +121,10 @@ export const getMetadataListRequestHandler = function ( } ); - const hostListQueryResult = queryStrategy!.queryResponseToHostListResult( - await context.core.elasticsearch.legacy.client.callAsCurrentUser('search', queryParams) + const result = await context.core.elasticsearch.client.asCurrentUser.search( + queryParams ); + const hostListQueryResult = queryStrategy!.queryResponseToHostListResult(result.body); return response.ok({ body: await mapToHostResultList(queryParams, hostListQueryResult, metadataRequestContext), }); @@ -136,9 +148,11 @@ export const getMetadataRequestHandler = function ( } const metadataRequestContext: MetadataRequestContext = { + esClient: context.core.elasticsearch.client, endpointAppContextService: endpointAppContext.service, logger, requestHandlerContext: context, + savedObjectsClient: context.core.savedObjects.client, }; try { @@ -164,42 +178,86 @@ export const getMetadataRequestHandler = function ( }; }; -export async function getHostData( +export async function getHostMetaData( metadataRequestContext: MetadataRequestContext, id: string, queryStrategyVersion?: MetadataQueryStrategyVersions -): Promise { +): Promise { + if ( + !metadataRequestContext.esClient && + !metadataRequestContext.requestHandlerContext?.core.elasticsearch.client + ) { + throw Boom.badRequest('esClient not found'); + } + + if ( + !metadataRequestContext.savedObjectsClient && + !metadataRequestContext.requestHandlerContext?.core.savedObjects + ) { + throw Boom.badRequest('savedObjectsClient not found'); + } + + const esClient = (metadataRequestContext?.esClient ?? + metadataRequestContext.requestHandlerContext?.core.elasticsearch + .client) as IScopedClusterClient; + + const esSavedObjectClient = + metadataRequestContext?.savedObjectsClient ?? + (metadataRequestContext.requestHandlerContext?.core.savedObjects + .client as SavedObjectsClientContract); + const queryStrategy = await metadataRequestContext.endpointAppContextService ?.getMetadataService() - ?.queryStrategy( - metadataRequestContext.requestHandlerContext.core.savedObjects.client, - queryStrategyVersion - ); - + ?.queryStrategy(esSavedObjectClient, queryStrategyVersion); const query = getESQueryHostMetadataByID(id, queryStrategy!); - const hostResult = queryStrategy!.queryResponseToHostResult( - await metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client.callAsCurrentUser( - 'search', - query - ) - ); + + const response = await esClient.asCurrentUser.search(query); + + const hostResult = queryStrategy!.queryResponseToHostResult(response.body); + const hostMetadata = hostResult.result; if (!hostMetadata) { return undefined; } - const agent = await findAgent(metadataRequestContext, hostMetadata); + return { metadata: hostMetadata, query_strategy_version: hostResult.queryStrategyVersion }; +} + +export async function getHostData( + metadataRequestContext: MetadataRequestContext, + id: string, + queryStrategyVersion?: MetadataQueryStrategyVersions +): Promise { + if (!metadataRequestContext.savedObjectsClient) { + throw Boom.badRequest('savedObjectsClient not found'); + } + + if ( + !metadataRequestContext.esClient && + !metadataRequestContext.requestHandlerContext?.core.elasticsearch.client + ) { + throw Boom.badRequest('esClient not found'); + } + + const hostResult = await getHostMetaData(metadataRequestContext, id, queryStrategyVersion); + + if (!hostResult) { + return undefined; + } + + const agent = await findAgent(metadataRequestContext, hostResult.metadata); if (agent && !agent.active) { throw Boom.badRequest('the requested endpoint is unenrolled'); } const metadata = await enrichHostMetadata( - hostMetadata, + hostResult.metadata, metadataRequestContext, - hostResult.queryStrategyVersion + hostResult.query_strategy_version ); - return { ...metadata, query_strategy_version: hostResult.queryStrategyVersion }; + + return { ...metadata, query_strategy_version: hostResult.query_strategy_version }; } async function findAgent( @@ -207,12 +265,20 @@ async function findAgent( hostMetadata: HostMetadata ): Promise { try { + if ( + !metadataRequestContext.esClient && + !metadataRequestContext.requestHandlerContext?.core.elasticsearch.client + ) { + throw new Error('esClient not found'); + } + + const esClient = (metadataRequestContext?.esClient ?? + metadataRequestContext.requestHandlerContext?.core.elasticsearch + .client) as IScopedClusterClient; + return await metadataRequestContext.endpointAppContextService ?.getAgentService() - ?.getAgent( - metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser, - hostMetadata.elastic.agent.id - ); + ?.getAgent(esClient.asCurrentUser, hostMetadata.elastic.agent.id); } catch (e) { if (e instanceof AgentNotFoundError) { metadataRequestContext.logger.warn( @@ -232,7 +298,7 @@ export async function mapToHostResultList( metadataRequestContext: MetadataRequestContext ): Promise { const totalNumberOfHosts = hostListQueryResult.resultLength; - if (hostListQueryResult.resultList.length > 0) { + if ((hostListQueryResult.resultList?.length ?? 0) > 0) { return { request_page_size: queryParams.size, request_page_index: queryParams.from, @@ -267,6 +333,35 @@ export async function enrichHostMetadata( let hostStatus = HostStatus.UNHEALTHY; let elasticAgentId = hostMetadata?.elastic?.agent?.id; const log = metadataRequestContext.logger; + + try { + if ( + !metadataRequestContext.esClient && + !metadataRequestContext.requestHandlerContext?.core.elasticsearch.client + ) { + throw new Error('esClient not found'); + } + + if ( + !metadataRequestContext.savedObjectsClient && + !metadataRequestContext.requestHandlerContext?.core.savedObjects + ) { + throw new Error('esSavedObjectClient not found'); + } + } catch (e) { + log.error(e); + throw e; + } + + const esClient = (metadataRequestContext?.esClient ?? + metadataRequestContext.requestHandlerContext?.core.elasticsearch + .client) as IScopedClusterClient; + + const esSavedObjectClient = + metadataRequestContext?.savedObjectsClient ?? + (metadataRequestContext.requestHandlerContext?.core.savedObjects + .client as SavedObjectsClientContract); + try { /** * Get agent status by elastic agent id if available or use the endpoint-agent id. @@ -279,10 +374,7 @@ export async function enrichHostMetadata( const status = await metadataRequestContext.endpointAppContextService ?.getAgentService() - ?.getAgentStatusById( - metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser, - elasticAgentId - ); + ?.getAgentStatusById(esClient.asCurrentUser, elasticAgentId); hostStatus = HOST_STATUS_MAPPING.get(status!) || HostStatus.UNHEALTHY; } catch (e) { if (e instanceof AgentNotFoundError) { @@ -297,17 +389,10 @@ export async function enrichHostMetadata( try { const agent = await metadataRequestContext.endpointAppContextService ?.getAgentService() - ?.getAgent( - metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser, - elasticAgentId - ); + ?.getAgent(esClient.asCurrentUser, elasticAgentId); const agentPolicy = await metadataRequestContext.endpointAppContextService .getAgentPolicyService() - ?.get( - metadataRequestContext.requestHandlerContext.core.savedObjects.client, - agent?.policy_id!, - true - ); + ?.get(esSavedObjectClient, agent?.policy_id!, true); const endpointPolicy = ((agentPolicy?.package_policies || []) as PackagePolicy[]).find( (policy: PackagePolicy) => policy.package?.name === 'endpoint' ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index f4698cbed6203..b916ec19da17f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -6,8 +6,6 @@ */ import { - ILegacyClusterClient, - ILegacyScopedClusterClient, KibanaResponseFactory, RequestHandler, RouteConfig, @@ -50,12 +48,17 @@ import { PackageService } from '../../../../../fleet/server/services'; import { metadataTransformPrefix } from '../../../../common/endpoint/constants'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; +import { + ClusterClientMock, + ScopedClusterClientMock, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../../src/core/server/elasticsearch/client/mocks'; describe('test endpoint route', () => { let routerMock: jest.Mocked; let mockResponse: jest.Mocked; - let mockClusterClient: jest.Mocked; - let mockScopedClient: jest.Mocked; + let mockClusterClient: ClusterClientMock; + let mockScopedClient: ScopedClusterClientMock; let mockSavedObjectClient: jest.Mocked; let mockPackageService: jest.Mocked; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -76,8 +79,8 @@ describe('test endpoint route', () => { }; beforeEach(() => { - mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient() as jest.Mocked; + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockClusterClient = elasticsearchServiceMock.createClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); @@ -119,7 +122,9 @@ describe('test endpoint route', () => { it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({}); const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: response }) + ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) )!; @@ -131,7 +136,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -157,7 +162,9 @@ describe('test endpoint route', () => { mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: true, } as unknown) as Agent); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: response }) + ); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) @@ -169,7 +176,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -214,7 +221,9 @@ describe('test endpoint route', () => { it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({}); const response = createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: response }) + ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) )!; @@ -226,7 +235,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -258,8 +267,10 @@ describe('test endpoint route', () => { mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata())) + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ + body: createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata()), + }) ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) @@ -270,10 +281,10 @@ describe('test endpoint route', () => { mockRequest, mockResponse ); - - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect( - mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must_not + (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool + .must_not ).toContainEqual({ terms: { 'elastic.agent.id': [ @@ -315,8 +326,10 @@ describe('test endpoint route', () => { mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata())) + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ + body: createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata()), + }) ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) @@ -328,10 +341,10 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(mockScopedClient.asCurrentUser.search).toBeCalled(); expect( // KQL filter to be passed through - mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must + (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must ).toContainEqual({ bool: { must_not: { @@ -349,7 +362,7 @@ describe('test endpoint route', () => { }, }); expect( - mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must + (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must ).toContainEqual({ bool: { must_not: [ @@ -393,8 +406,8 @@ describe('test endpoint route', () => { it('should return 404 on no results', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } }); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createV2SearchResponse()) + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: createV2SearchResponse() }) ); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); @@ -411,7 +424,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -431,7 +444,9 @@ describe('test endpoint route', () => { mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: true, } as unknown) as Agent); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: response }) + ); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) @@ -443,7 +458,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -470,7 +485,9 @@ describe('test endpoint route', () => { SavedObjectsErrorHelpers.createGenericNotFoundError(); }); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: response }) + ); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) @@ -482,7 +499,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -503,7 +520,9 @@ describe('test endpoint route', () => { mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: true, } as unknown) as Agent); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: response }) + ); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) @@ -515,7 +534,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -531,7 +550,9 @@ describe('test endpoint route', () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: response.hits.hits[0]._id }, }); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: response }) + ); mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: false, } as unknown) as Agent); @@ -546,7 +567,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(mockResponse.customError).toBeCalled(); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts index e3f859c26601e..0d56514e7d395 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts @@ -6,14 +6,17 @@ */ import { - ILegacyClusterClient, - ILegacyScopedClusterClient, KibanaResponseFactory, RequestHandler, RouteConfig, SavedObjectsClientContract, -} from 'kibana/server'; -import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server/'; + SavedObjectsErrorHelpers, +} from '../../../../../../../src/core/server'; +import { + ClusterClientMock, + ScopedClusterClientMock, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../../src/core/server/elasticsearch/client/mocks'; import { elasticsearchServiceMock, httpServerMock, @@ -49,8 +52,8 @@ import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; describe('test endpoint route v1', () => { let routerMock: jest.Mocked; let mockResponse: jest.Mocked; - let mockClusterClient: jest.Mocked; - let mockScopedClient: jest.Mocked; + let mockClusterClient: ClusterClientMock; + let mockScopedClient: ScopedClusterClientMock; let mockSavedObjectClient: jest.Mocked; let mockPackageService: jest.Mocked; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -71,8 +74,8 @@ describe('test endpoint route v1', () => { }; beforeEach(() => { - mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient() as jest.Mocked; - mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockClusterClient = elasticsearchServiceMock.createClusterClient(); + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); @@ -110,7 +113,9 @@ describe('test endpoint route v1', () => { it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({}); const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: response }) + ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) )!; @@ -122,7 +127,7 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] }); expect(mockResponse.ok).toBeCalled(); const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; @@ -151,8 +156,10 @@ describe('test endpoint route v1', () => { mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata())) + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ + body: createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()), + }) ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) @@ -164,9 +171,10 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect( - mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must_not + (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool + .must_not ).toContainEqual({ terms: { 'elastic.agent.id': [ @@ -205,8 +213,10 @@ describe('test endpoint route v1', () => { mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata())) + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ + body: createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()), + }) ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) @@ -218,10 +228,10 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(mockScopedClient.asCurrentUser.search).toBeCalled(); // needs to have the KQL filter passed through expect( - mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must + (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must ).toContainEqual({ bool: { must_not: { @@ -240,7 +250,7 @@ describe('test endpoint route v1', () => { }); // and unenrolled should be filtered out. expect( - mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must + (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must ).toContainEqual({ bool: { must_not: [ @@ -281,8 +291,8 @@ describe('test endpoint route v1', () => { it('should return 404 on no results', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } }); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createV1SearchResponse()) + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: createV1SearchResponse() }) ); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); @@ -299,7 +309,7 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -319,7 +329,9 @@ describe('test endpoint route v1', () => { mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: true, } as unknown) as Agent); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: response }) + ); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) @@ -331,7 +343,7 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -357,7 +369,9 @@ describe('test endpoint route v1', () => { SavedObjectsErrorHelpers.createGenericNotFoundError(); }); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: response }) + ); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) @@ -369,7 +383,7 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -390,7 +404,9 @@ describe('test endpoint route v1', () => { mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: true, } as unknown) as Agent); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: response }) + ); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) @@ -402,7 +418,7 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -418,7 +434,9 @@ describe('test endpoint route v1', () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: response.hits.hits[0]._id }, }); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: response }) + ); mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: false, } as unknown) as Agent); @@ -433,7 +451,7 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(mockResponse.customError).toBeCalled(); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts index 5c09fd5ce05e4..e790c1de1a5b8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts @@ -12,6 +12,7 @@ import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__ import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constants'; import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; import { metadataQueryStrategyV2 } from './support/query_strategies'; +import { get } from 'lodash'; describe('query builder', () => { describe('MetadataListESQuery', () => { @@ -204,7 +205,7 @@ describe('query builder', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV2()); - expect(query.body.query.bool.filter[0].bool.should).toContainEqual({ + expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({ term: { 'agent.id': mockID }, }); }); @@ -213,7 +214,7 @@ describe('query builder', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV2()); - expect(query.body.query.bool.filter[0].bool.should).toContainEqual({ + expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({ term: { 'HostDetails.agent.id': mockID }, }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index a5259dd44cf2b..51e3495938606 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { KibanaRequest } from 'kibana/server'; +import { SearchRequest, SortContainer } from '@elastic/elasticsearch/api/types'; +import { KibanaRequest } from '../../../../../../../src/core/server'; import { esKuery } from '../../../../../../../src/plugins/data/server'; import { EndpointAppContext, MetadataQueryStrategy } from '../../types'; @@ -19,7 +20,7 @@ export interface QueryBuilderOptions { // using unmapped_type avoids errors when the given field doesn't exist, and sets to the 0-value for that type // effectively ignoring it // https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html#_ignoring_unmapped_fields -const MetadataSortMethod = [ +const MetadataSortMethod: SortContainer[] = [ { 'event.created': { order: 'desc', @@ -146,7 +147,7 @@ function buildQueryBody( export function getESQueryHostMetadataByID( agentID: string, metadataQueryStrategy: MetadataQueryStrategy -) { +): SearchRequest { return { body: { query: { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts index 9ce6130ff7dd3..c18c585cd3d34 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts @@ -12,6 +12,7 @@ import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__ import { metadataIndexPattern } from '../../../../common/endpoint/constants'; import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; import { metadataQueryStrategyV1 } from './support/query_strategies'; +import { get } from 'lodash'; describe('query builder v1', () => { describe('MetadataListESQuery', () => { @@ -179,7 +180,7 @@ describe('query builder v1', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV1()); - expect(query.body.query.bool.filter[0].bool.should).toContainEqual({ + expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({ term: { 'agent.id': mockID }, }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts index 2f875ec2754a4..506c02fc2f1ec 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; +import { SearchResponse } from '@elastic/elasticsearch/api/types'; import { metadataCurrentIndexPattern, metadataIndexPattern, @@ -13,10 +13,6 @@ import { import { HostMetadata, MetadataQueryStrategyVersions } from '../../../../../common/endpoint/types'; import { HostListQueryResult, HostQueryResult, MetadataQueryStrategy } from '../../../types'; -interface HitSource { - _source: HostMetadata; -} - export function metadataQueryStrategyV1(): MetadataQueryStrategy { return { index: metadataIndexPattern, @@ -42,11 +38,13 @@ export function metadataQueryStrategyV1(): MetadataQueryStrategy { ): HostListQueryResult => { const response = searchResponse as SearchResponse; return { - resultLength: response?.aggregations?.total?.value || 0, + resultLength: + ((response?.aggregations?.total as unknown) as { value?: number; relation: string }) + ?.value || 0, resultList: response.hits.hits - .map((hit) => hit.inner_hits.most_recent.hits.hits) - .flatMap((data) => data as HitSource) - .map((entry) => entry._source), + .map((hit) => hit.inner_hits?.most_recent.hits.hits) + .flatMap((data) => data) + .map((entry) => (entry?._source ?? {}) as HostMetadata), queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_1, }; }, @@ -75,7 +73,7 @@ export function metadataQueryStrategyV2(): MetadataQueryStrategy { >; const list = response.hits.hits.length > 0 - ? response.hits.hits.map((entry) => stripHostDetails(entry._source)) + ? response.hits.hits.map((entry) => stripHostDetails(entry?._source as HostMetadata)) : []; return { @@ -95,7 +93,7 @@ export function metadataQueryStrategyV2(): MetadataQueryStrategy { resultLength: response.hits.hits.length, result: response.hits.hits.length > 0 - ? stripHostDetails(response.hits.hits[0]._source) + ? stripHostDetails(response.hits.hits[0]._source as HostMetadata) : undefined, queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_2, }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts index ca9b8832bebd0..c8b36a22b359a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts @@ -13,10 +13,9 @@ import { import { createMockAgentService } from '../../../../../fleet/server/mocks'; import { getHostPolicyResponseHandler, getAgentPolicySummaryHandler } from './handlers'; import { - ILegacyScopedClusterClient, KibanaResponseFactory, SavedObjectsClientContract, -} from 'kibana/server'; +} from '../../../../../../../src/core/server'; import { elasticsearchServiceMock, httpServerMock, @@ -30,16 +29,19 @@ import { parseExperimentalConfigValue } from '../../../../common/experimental_fe import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { Agent } from '../../../../../fleet/common/types/models'; import { AgentService } from '../../../../../fleet/server/services'; +import { get } from 'lodash'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ScopedClusterClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks'; describe('test policy response handler', () => { let endpointAppContextService: EndpointAppContextService; - let mockScopedClient: jest.Mocked; + let mockScopedClient: ScopedClusterClientMock; let mockSavedObjectClient: jest.Mocked; let mockResponse: jest.Mocked; describe('test policy response handler', () => { beforeEach(() => { - mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockResponse = httpServerMock.createResponseFactory(); endpointAppContextService = new EndpointAppContextService(); @@ -52,7 +54,9 @@ describe('test policy response handler', () => { const response = createSearchResponse(new EndpointDocGenerator().generatePolicyResponse()); const hostPolicyResponseHandler = getHostPolicyResponseHandler(); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: response }) + ); const mockRequest = httpServerMock.createKibanaRequest({ params: { agentId: 'id' }, }); @@ -65,14 +69,16 @@ describe('test policy response handler', () => { expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as GetHostPolicyResponse; - expect(result.policy_response.agent.id).toEqual(response.hits.hits[0]._source.agent.id); + expect(result.policy_response.agent.id).toEqual( + get(response, 'hits.hits.0._source.agent.id') + ); }); it('should return not found when there is no response policy for host', async () => { const hostPolicyResponseHandler = getHostPolicyResponseHandler(); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createSearchResponse()) + (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: createSearchResponse() }) ); const mockRequest = httpServerMock.createKibanaRequest({ @@ -109,7 +115,7 @@ describe('test policy response handler', () => { }; beforeEach(() => { - mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockResponse = httpServerMock.createResponseFactory(); endpointAppContextService = new EndpointAppContextService(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts index ec1fad80701b6..45b6201c47773 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts @@ -25,7 +25,7 @@ export const getHostPolicyResponseHandler = function (): RequestHandler< const doc = await getPolicyResponseByAgentId( policyIndexPattern, request.query.agentId, - context.core.elasticsearch.legacy.client + context.core.elasticsearch.client ); if (doc) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts index 8043eae20b30e..8646a05900f80 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts @@ -24,7 +24,7 @@ describe('test policy query', () => { it('queries for the correct host', async () => { const agentId = 'f757d3c0-e874-11ea-9ad9-015510b487f4'; const query = getESQueryPolicyResponseByAgentID(agentId, 'anyindex'); - expect(query.body.query.bool.filter.term).toEqual({ 'agent.id': agentId }); + expect(query.body?.query?.bool?.filter).toEqual({ term: { 'agent.id': agentId } }); }); it('filters out initial policy by ID', async () => { @@ -32,8 +32,10 @@ describe('test policy query', () => { 'f757d3c0-e874-11ea-9ad9-015510b487f4', 'anyindex' ); - expect(query.body.query.bool.must_not.term).toEqual({ - 'Endpoint.policy.applied.id': '00000000-0000-0000-0000-000000000000', + expect(query.body?.query?.bool?.must_not).toEqual({ + term: { + 'Endpoint.policy.applied.id': '00000000-0000-0000-0000-000000000000', + }, }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts index af5a885b78040..987bef15afe98 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts @@ -5,18 +5,21 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; import { ElasticsearchClient, - ILegacyScopedClusterClient, + IScopedClusterClient, SavedObjectsClientContract, -} from 'kibana/server'; +} from '../../../../../../../src/core/server'; import { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types'; import { INITIAL_POLICY_ID } from './index'; import { Agent } from '../../../../../fleet/common/types/models'; import { EndpointAppContext } from '../../types'; +import { ISearchRequestParams } from '../../../../../../../src/plugins/data/common'; -export function getESQueryPolicyResponseByAgentID(agentID: string, index: string) { +export const getESQueryPolicyResponseByAgentID = ( + agentID: string, + index: string +): ISearchRequestParams => { return { body: { query: { @@ -44,26 +47,23 @@ export function getESQueryPolicyResponseByAgentID(agentID: string, index: string }, index, }; -} +}; export async function getPolicyResponseByAgentId( index: string, agentID: string, - dataClient: ILegacyScopedClusterClient + dataClient: IScopedClusterClient ): Promise { const query = getESQueryPolicyResponseByAgentID(agentID, index); - const response = (await dataClient.callAsCurrentUser( - 'search', - query - )) as SearchResponse; + const response = await dataClient.asCurrentUser.search(query); - if (response.hits.hits.length === 0) { - return undefined; + if (response.body.hits.hits.length > 0 && response.body.hits.hits[0]._source != null) { + return { + policy_response: response.body.hits.hits[0]._source, + }; } - return { - policy_response: response.hits.hits[0]._source, - }; + return undefined; } const transformAgentVersionMap = (versionMap: Map): { [key: string]: number } => { diff --git a/x-pack/plugins/security_solution/server/endpoint/types.ts b/x-pack/plugins/security_solution/server/endpoint/types.ts index 8006bf20d4517..b3c7e58afe991 100644 --- a/x-pack/plugins/security_solution/server/endpoint/types.ts +++ b/x-pack/plugins/security_solution/server/endpoint/types.ts @@ -6,7 +6,8 @@ */ import { LoggerFactory } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; + +import { SearchResponse } from '@elastic/elasticsearch/api/types'; import { ConfigType } from '../config'; import { EndpointAppContextService } from './endpoint_app_context_services'; import { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 46467a21ca7ab..158c2e94b2d7a 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -305,7 +305,10 @@ export class Plugin implements IPlugin { - const securitySolutionSearchStrategy = securitySolutionSearchStrategyProvider(depsStart.data); + const securitySolutionSearchStrategy = securitySolutionSearchStrategyProvider( + depsStart.data, + endpointContext + ); const securitySolutionTimelineSearchStrategy = securitySolutionTimelineSearchStrategyProvider( depsStart.data ); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx index 9e85eefe21e8a..fa78a8d59803d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx @@ -58,7 +58,6 @@ export const authentications: SecuritySolutionFactory fakeTotalCount; - return { ...response, inspect, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts index 7561682e070fc..9dfff5e11715d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts @@ -1370,6 +1370,20 @@ export const formattedSearchStrategyResponse = { terms: { field: 'cloud.region', size: 10, order: { timestamp: 'desc' } }, aggs: { timestamp: { max: { field: '@timestamp' } } }, }, + endpoint_id: { + filter: { + term: { + 'agent.type': 'endpoint', + }, + }, + aggs: { + value: { + terms: { + field: 'agent.id', + }, + }, + }, + }, }, query: { bool: { @@ -1413,6 +1427,20 @@ export const expectedDsl = { track_total_hits: false, body: { aggregations: { + endpoint_id: { + filter: { + term: { + 'agent.type': 'endpoint', + }, + }, + aggs: { + value: { + terms: { + field: 'agent.id', + }, + }, + }, + }, host_architecture: { terms: { field: 'host.architecture', diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index a581370cb5720..1b6e927f33638 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -7,16 +7,23 @@ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; +import { + IScopedClusterClient, + SavedObjectsClientContract, +} from '../../../../../../../../../src/core/server'; import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; -import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; import { Direction } from '../../../../../../common/search_strategy/common'; import { AggregationRequest, + EndpointFields, HostAggEsItem, HostBuckets, HostItem, HostValue, } from '../../../../../../common/search_strategy/security_solution/hosts'; +import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; +import { getHostMetaData } from '../../../../../endpoint/routes/metadata/handlers'; +import { EndpointAppContext } from '../../../../../endpoint/types'; export const HOST_FIELDS = [ '_id', @@ -38,6 +45,8 @@ export const HOST_FIELDS = [ 'endpoint.endpointPolicy', 'endpoint.policyStatus', 'endpoint.sensorVersion', + 'agent.type', + 'endpoint.id', ]; export const buildFieldsTermAggregation = (esFields: readonly string[]): AggregationRequest => @@ -99,8 +108,8 @@ const getTermsAggregationTypeFromField = (field: string): AggregationRequest => }; }; -export const formatHostItem = (bucket: HostAggEsItem): HostItem => - HOST_FIELDS.reduce((flattenedFields, fieldName) => { +export const formatHostItem = (bucket: HostAggEsItem): HostItem => { + return HOST_FIELDS.reduce((flattenedFields, fieldName) => { const fieldValue = getHostFieldValue(fieldName, bucket); if (fieldValue != null) { if (fieldName === '_id') { @@ -114,11 +123,13 @@ export const formatHostItem = (bucket: HostAggEsItem): HostItem => } return flattenedFields; }, {}); +}; const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | string[] | null => { const aggField = hostFieldsMap[fieldName] ? hostFieldsMap[fieldName].replace(/\./g, '_') : fieldName.replace(/\./g, '_'); + if ( [ 'host.ip', @@ -134,10 +145,7 @@ const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | s return data.buckets.map((obj) => obj.key); } else if (has(`${aggField}.buckets`, bucket)) { return getFirstItem(get(`${aggField}`, bucket)); - } else if (has(aggField, bucket)) { - const valueObj: HostValue = get(aggField, bucket); - return valueObj.value_as_string; - } else if (['host.name', 'host.os.name', 'host.os.version'].includes(fieldName)) { + } else if (['host.name', 'host.os.name', 'host.os.version', 'endpoint.id'].includes(fieldName)) { switch (fieldName) { case 'host.name': return get('key', bucket) || null; @@ -145,7 +153,12 @@ const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | s return get('os.hits.hits[0]._source.host.os.name', bucket) || null; case 'host.os.version': return get('os.hits.hits[0]._source.host.os.version', bucket) || null; + case 'endpoint.id': + return get('endpoint_id.value.buckets[0].key', bucket) || null; } + } else if (has(aggField, bucket)) { + const valueObj: HostValue = get(aggField, bucket); + return valueObj.value_as_string; } else if (aggField === '_id') { const hostName = get(`host_name`, bucket); return hostName ? getFirstItem(hostName) : null; @@ -160,3 +173,42 @@ const getFirstItem = (data: HostBuckets): string | null => { } return firstItem.key; }; + +export const getHostEndpoint = async ( + id: string | null, + deps: { + esClient: IScopedClusterClient; + savedObjectsClient: SavedObjectsClientContract; + endpointContext: EndpointAppContext; + } +): Promise => { + const { esClient, endpointContext, savedObjectsClient } = deps; + const logger = endpointContext.logFactory.get('metadata'); + try { + const agentService = endpointContext.service.getAgentService(); + if (agentService === undefined) { + throw new Error('agentService not available'); + } + const metadataRequestContext = { + esClient, + endpointAppContextService: endpointContext.service, + logger, + savedObjectsClient, + }; + const endpointData = + id != null && metadataRequestContext.endpointAppContextService.getAgentService() != null + ? await getHostMetaData(metadataRequestContext, id, undefined) + : null; + + return endpointData != null && endpointData.metadata + ? { + endpointPolicy: endpointData.metadata.Endpoint.policy.applied.name, + policyStatus: endpointData.metadata.Endpoint.policy.applied.status, + sensorVersion: endpointData.metadata.agent.version, + } + : null; + } catch (err) { + logger.warn(JSON.stringify(err, null, 2)); + return null; + } +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx index 244b826c7caeb..4474b9f288570 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx @@ -12,6 +12,32 @@ import { mockSearchStrategyResponse, formattedSearchStrategyResponse, } from './__mocks__'; +import { + IScopedClusterClient, + SavedObjectsClientContract, +} from '../../../../../../../../../src/core/server'; +import { EndpointAppContext } from '../../../../../endpoint/types'; +import { EndpointAppContextService } from '../../../../../endpoint/endpoint_app_context_services'; + +const mockDeps = { + esClient: {} as IScopedClusterClient, + savedObjectsClient: {} as SavedObjectsClientContract, + endpointContext: { + logFactory: { + get: jest.fn().mockReturnValue({ + warn: jest.fn(), + }), + }, + config: jest.fn().mockResolvedValue({}), + experimentalFeatures: { + trustedAppsByPolicyEnabled: false, + metricsEntitiesEnabled: false, + eventFilteringEnabled: false, + hostIsolationEnabled: false, + }, + service: {} as EndpointAppContextService, + } as EndpointAppContext, +}; describe('hostDetails search strategy', () => { const buildHostDetailsQuery = jest.spyOn(buildQuery, 'buildHostDetailsQuery'); @@ -29,7 +55,7 @@ describe('hostDetails search strategy', () => { describe('parse', () => { test('should parse data correctly', async () => { - const result = await hostDetails.parse(mockOptions, mockSearchStrategyResponse); + const result = await hostDetails.parse(mockOptions, mockSearchStrategyResponse, mockDeps); expect(result).toMatchObject(formattedSearchStrategyResponse); }); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts index 5da64cc8f7a90..562b7e4fbc167 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts @@ -10,28 +10,58 @@ import { get } from 'lodash/fp'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { HostAggEsData, - HostAggEsItem, HostDetailsStrategyResponse, HostsQueries, HostDetailsRequestOptions, + EndpointFields, } from '../../../../../../common/search_strategy/security_solution/hosts'; import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionFactory } from '../../types'; import { buildHostDetailsQuery } from './query.host_details.dsl'; -import { formatHostItem } from './helpers'; +import { formatHostItem, getHostEndpoint } from './helpers'; +import { EndpointAppContext } from '../../../../../endpoint/types'; +import { + IScopedClusterClient, + SavedObjectsClientContract, +} from '../../../../../../../../../src/core/server'; export const hostDetails: SecuritySolutionFactory = { buildDsl: (options: HostDetailsRequestOptions) => buildHostDetailsQuery(options), parse: async ( options: HostDetailsRequestOptions, - response: IEsSearchResponse + response: IEsSearchResponse, + deps?: { + esClient: IScopedClusterClient; + savedObjectsClient: SavedObjectsClientContract; + endpointContext: EndpointAppContext; + } ): Promise => { - const aggregations: HostAggEsItem = get('aggregations', response.rawResponse) || {}; + const aggregations = get('aggregations', response.rawResponse); + const inspect = { dsl: [inspectStringifyObject(buildHostDetailsQuery(options))], }; + + if (aggregations == null) { + return { ...response, inspect, hostDetails: {} }; + } + const formattedHostItem = formatHostItem(aggregations); - return { ...response, inspect, hostDetails: formattedHostItem }; + const ident = // endpoint-generated ID, NOT elastic-agent-id + formattedHostItem.endpoint && formattedHostItem.endpoint.id + ? Array.isArray(formattedHostItem.endpoint.id) + ? formattedHostItem.endpoint.id[0] + : formattedHostItem.endpoint.id + : null; + if (deps == null) { + return { ...response, inspect, hostDetails: { ...formattedHostItem } }; + } + const endpoint: EndpointFields | null = await getHostEndpoint(ident, deps); + return { + ...response, + inspect, + hostDetails: endpoint != null ? { ...formattedHostItem, endpoint } : formattedHostItem, + }; }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts index fb8296d6593b0..45afed2526aa3 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts @@ -16,7 +16,10 @@ export const buildHostDetailsQuery = ({ defaultIndex, timerange: { from, to }, }: HostDetailsRequestOptions): ISearchRequestParams => { - const esFields = reduceFields(HOST_FIELDS, { ...hostFieldsMap, ...cloudFieldsMap }); + const esFields = reduceFields(HOST_FIELDS, { + ...hostFieldsMap, + ...cloudFieldsMap, + }); const filter = [ { term: { 'host.name': hostName } }, @@ -39,6 +42,20 @@ export const buildHostDetailsQuery = ({ body: { aggregations: { ...buildFieldsTermAggregation(esFields.filter((field) => !['@timestamp'].includes(field))), + endpoint_id: { + filter: { + term: { + 'agent.type': 'endpoint', + }, + }, + aggs: { + value: { + terms: { + field: 'agent.id', + }, + }, + }, + }, }, query: { bool: { filter } }, size: 0, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts index 3455b627144bf..4bdf97b489805 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts @@ -5,6 +5,10 @@ * 2.0. */ +import { + IScopedClusterClient, + SavedObjectsClientContract, +} from '../../../../../../../src/core/server'; import { IEsSearchResponse, ISearchRequestParams, @@ -14,11 +18,17 @@ import { StrategyRequestType, StrategyResponseType, } from '../../../../common/search_strategy/security_solution'; +import { EndpointAppContext } from '../../../endpoint/types'; export interface SecuritySolutionFactory { buildDsl: (options: StrategyRequestType) => ISearchRequestParams; parse: ( options: StrategyRequestType, - response: IEsSearchResponse + response: IEsSearchResponse, + deps?: { + esClient: IScopedClusterClient; + savedObjectsClient: SavedObjectsClientContract; + endpointContext: EndpointAppContext; + } ) => Promise>; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts index 2980f63df8a67..0883a144615bc 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts @@ -19,9 +19,11 @@ import { } from '../../../common/search_strategy/security_solution'; import { securitySolutionFactory } from './factory'; import { SecuritySolutionFactory } from './factory/types'; +import { EndpointAppContext } from '../../endpoint/types'; export const securitySolutionSearchStrategyProvider = ( - data: PluginStart + data: PluginStart, + endpointContext: EndpointAppContext ): ISearchStrategy, StrategyResponseType> => { const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); @@ -42,7 +44,13 @@ export const securitySolutionSearchStrategyProvider = queryFactory.parse(request, esSearchRes)) + mergeMap((esSearchRes) => + queryFactory.parse(request, esSearchRes, { + esClient: deps.esClient, + savedObjectsClient: deps.savedObjectsClient, + endpointContext, + }) + ) ); }, cancel: async (id, options, deps) => { From 38116e89fdb8a36db94a8d1453912bd71ea53013 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Mon, 10 May 2021 13:01:56 -0700 Subject: [PATCH 23/69] [DOCS] Adds link between security docs (#99669) --- docs/user/security/authentication/index.asciidoc | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index 805ae924a599e..54142a6fe39e3 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -7,6 +7,7 @@ {kib} supports the following authentication mechanisms: +- <> - <> - <> - <> @@ -16,7 +17,12 @@ - <> - <> -Enable multiple authentication mechanisms at the same time specifying a prioritized list of the authentication _providers_ (typically of various types) in the configuration. Providers are consulted in ascending order. Make sure each configured provider has a unique name (e.g. `basic1` or `saml1` in the configuration example) and `order` setting. In the event that two or more providers have the same name or `order`, {kib} will fail to start. +For an introduction to {kib}'s security features, including the login process, refer to <>. + +[[multiple-authentication-providers]] +==== Multiple authentication providers + +Enable multiple authentication mechanisms at the same time by specifying a prioritized list of the authentication _providers_ (typically of various types) in the configuration. Providers are consulted in ascending order. Make sure each configured provider has a unique name (e.g. `basic1` or `saml1` in the configuration example) and `order` setting. In the event that two or more providers have the same name or `order`, {kib} will fail to start. When two or more providers are configured, you can choose the provider you want to use on the Login Selector UI. The order the providers appear is determined by the `order` setting. The appearance of the specific provider entry can be customized with the `description`, `hint`, and `icon` settings. @@ -24,7 +30,7 @@ TIP: To provide login instructions to users, use the `xpack.security.loginHelp` If you don't want a specific provider to show up at the Login Selector UI (e.g. to only support third-party initiated login) you can hide it with `showInSelector` setting set to `false`. However, in this case, the provider is presented in the provider chain and may be consulted during authentication based on its `order`. To disable the provider, use the `enabled` setting. -TIP: The Login Selector UI can also be disabled or enabled with `xpack.security.authc.selector.enabled` setting. +TIP: The Login Selector UI can also be disabled or enabled with `xpack.security.authc.selector.enabled` setting. Here is how your `kibana.yml` and Login Selector UI can look like if you deal with multiple authentication providers: @@ -292,9 +298,9 @@ xpack.security.authc.providers: order: 1 ----------------------------------------------- -IMPORTANT: {kib} uses SPNEGO, which wraps the Kerberos protocol for use with HTTP, extending it to web applications. +IMPORTANT: {kib} uses SPNEGO, which wraps the Kerberos protocol for use with HTTP, extending it to web applications. At the end of the Kerberos handshake, {kib} forwards the service ticket to {es}, then {es} unpacks the service ticket and responds with an access and refresh token, which are used for subsequent authentication. -On every {es} node that {kib} connects to, the keytab file should always contain the HTTP service principal for the {kib} host. +On every {es} node that {kib} connects to, the keytab file should always contain the HTTP service principal for the {kib} host. The HTTP service principal name must have the `HTTP/kibana.domain.local@KIBANA.DOMAIN.LOCAL` format. @@ -386,7 +392,7 @@ xpack.security.authc.providers: [[anonymous-access-and-embedding]] ===== Anonymous access and embedding -One of the most popular use cases for anonymous access is when you embed {kib} into other applications and don't want to force your users to log in to view it. +One of the most popular use cases for anonymous access is when you embed {kib} into other applications and don't want to force your users to log in to view it. If you configured {kib} to use anonymous access as the sole authentication mechanism, you don't need to do anything special while embedding {kib}. If you have multiple authentication providers enabled, and you want to automatically log in anonymous users when embedding dashboards and visualizations: From 7262ac53c2ade6cce273a4ca56ab0fbfc128c3b2 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Mon, 10 May 2021 16:38:32 -0400 Subject: [PATCH 24/69] [CI] Don't do CI stats reporting/failures for feature branch PRs (#99668) --- Jenkinsfile | 2 +- vars/githubPr.groovy | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 4c8f126b4883b..db5ae306e6e2e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -6,7 +6,7 @@ kibanaLibrary.load() kibanaPipeline(timeoutMinutes: 210, checkPrChanges: true, setCommitStatus: true) { slackNotifications.onFailure(disabled: !params.NOTIFY_ON_FAILURE) { githubPr.withDefaultPrComments { - ciStats.trackBuild(requireSuccess: githubPr.isPr()) { + ciStats.trackBuild(requireSuccess: githubPr.isTrackedBranchPr()) { catchError { retryable.enable() kibanaPipeline.allCiTasks() diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index a2a3a81f253a0..594d54f2c5b5e 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -64,6 +64,10 @@ def isPr() { return !!(env.ghprbPullId && env.ghprbPullLink && env.ghprbPullLink =~ /\/elastic\/kibana\//) } +def isTrackedBranchPr() { + return isPr() && (env.ghprbTargetBranch == 'master' || env.ghprbTargetBranch == '6.8' || env.ghprbTargetBranch =~ /[7-8]\.[x0-9]+/) +} + def getLatestBuildComment() { return getComments() .reverse() @@ -234,8 +238,10 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { messages << getTestFailuresMessage() - if (isFinal) { - messages << ciStats.getMetricsReport() + catchErrors { + if (isFinal && isTrackedBranchPr()) { + messages << ciStats.getMetricsReport() + } } if (info.builds && info.builds.size() > 0) { From 59f42ec148df30c60617e3c56794e2b938037e40 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Mon, 10 May 2021 14:12:24 -0700 Subject: [PATCH 25/69] [Saved object migrations] Collect all documents that fail to transform before stopping the migration (#96986) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../migrations/core/document_migrator.test.ts | 14 +- .../migrations/core/document_migrator.ts | 13 +- .../saved_objects/migrations/core/index.ts | 6 + .../migrations/core/migrate_raw_docs.test.ts | 162 +++++++++- .../migrations/core/migrate_raw_docs.ts | 121 +++++++- ...nsform_saved_object_document_error.test.ts | 60 ++++ .../transform_saved_object_document_error.ts | 32 ++ .../migrations/kibana/kibana_migrator.ts | 9 +- .../migrationsv2/actions/index.test.ts | 12 - .../migrationsv2/actions/index.ts | 30 +- .../integration_tests/actions.test.ts | 47 ++- ....0_migrated_with_corrupt_outdated_docs.zip | Bin 0 -> 444632 bytes .../integration_tests/cleanup.test.ts | 2 +- .../corrupt_outdated_docs.test.ts | 154 ++++++++++ .../migrations_state_action_machine.ts | 7 - .../saved_objects/migrationsv2/model.test.ts | 283 ++++++++++++++++-- .../saved_objects/migrationsv2/model.ts | 214 ++++++++++--- .../server/saved_objects/migrationsv2/next.ts | 16 +- .../saved_objects/migrationsv2/types.ts | 39 ++- 19 files changed, 1048 insertions(+), 173 deletions(-) create mode 100644 src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.test.ts create mode 100644 src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.ts create mode 100644 src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip create mode 100644 src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 1cf408ea96a56..45286f158edb1 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -11,6 +11,7 @@ import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import { SavedObjectUnsanitizedDoc } from '../../serialization'; import { DocumentMigrator } from './document_migrator'; +import { TransformSavedObjectDocumentError } from './transform_saved_object_document_error'; import { loggingSystemMock } from '../../../logging/logging_system.mock'; import { SavedObjectsType } from '../../types'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; @@ -724,6 +725,12 @@ describe('DocumentMigrator', () => { it('logs the original error and throws a transform error if a document transform fails', () => { const log = mockLogger; + const failedDoc = { + id: 'smelly', + type: 'dog', + attributes: {}, + migrationVersion: {}, + }; const migrator = new DocumentMigrator({ ...testOpts(), typeRegistry: createRegistry({ @@ -737,12 +744,6 @@ describe('DocumentMigrator', () => { log, }); migrator.prepareMigrations(); - const failedDoc = { - id: 'smelly', - type: 'dog', - attributes: {}, - migrationVersion: {}, - }; try { migrator.migrate(_.cloneDeep(failedDoc)); expect('Did not throw').toEqual('But it should have!'); @@ -751,6 +752,7 @@ describe('DocumentMigrator', () => { "Failed to transform document smelly. Transform: dog:1.2.3 Doc: {\\"id\\":\\"smelly\\",\\"type\\":\\"dog\\",\\"attributes\\":{},\\"migrationVersion\\":{}}" `); + expect(error).toBeInstanceOf(TransformSavedObjectDocumentError); expect(loggingSystemMock.collect(mockLoggerFactory).error[0][0]).toMatchInlineSnapshot( `[Error: Dang diggity!]` ); diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 1dd4a8fbf6388..4f58397866cfb 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -62,6 +62,7 @@ import { SavedObjectsType, } from '../../types'; import { MigrationLogger } from './migration_logger'; +import { TransformSavedObjectDocumentError } from '.'; import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectMigrationFn, SavedObjectMigrationMap } from '../types'; import { DEFAULT_NAMESPACE_STRING } from '../../service/lib/utils'; @@ -679,9 +680,15 @@ function wrapWithTry( const failedTransform = `${type.name}:${version}`; const failedDoc = JSON.stringify(doc); log.error(error); - - throw new Error( - `Failed to transform document ${doc?.id}. Transform: ${failedTransform}\nDoc: ${failedDoc}` + // To make debugging failed migrations easier, we add items needed to convert the + // saved object id to the full raw id (the id only contains the uuid part) and the full error itself + throw new TransformSavedObjectDocumentError( + doc.id, + doc.type, + doc.namespace, + failedTransform, + failedDoc, + error ); } }; diff --git a/src/core/server/saved_objects/migrations/core/index.ts b/src/core/server/saved_objects/migrations/core/index.ts index 1e51983a0ffbd..ca54d6876ad75 100644 --- a/src/core/server/saved_objects/migrations/core/index.ts +++ b/src/core/server/saved_objects/migrations/core/index.ts @@ -15,3 +15,9 @@ export type { MigrationResult, MigrationStatus } from './migration_coordinator'; export { createMigrationEsClient } from './migration_es_client'; export type { MigrationEsClient } from './migration_es_client'; export { excludeUnusedTypesQuery } from './elastic_index'; +export { TransformSavedObjectDocumentError } from './transform_saved_object_document_error'; +export type { + DocumentsTransformFailed, + DocumentsTransformSuccess, + TransformErrorObjects, +} from './migrate_raw_docs'; diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index 45e73f7dfae30..1d43e2f54a726 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -7,10 +7,17 @@ */ import { set } from '@elastic/safer-lodash-set'; +import * as Either from 'fp-ts/lib/Either'; import _ from 'lodash'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectsSerializer } from '../../serialization'; -import { migrateRawDocs } from './migrate_raw_docs'; +import { + DocumentsTransformFailed, + DocumentsTransformSuccess, + migrateRawDocs, + migrateRawDocsSafely, +} from './migrate_raw_docs'; +import { TransformSavedObjectDocumentError } from './transform_saved_object_document_error'; describe('migrateRawDocs', () => { test('converts raw docs to saved objects', async () => { @@ -120,3 +127,156 @@ describe('migrateRawDocs', () => { ).rejects.toThrowErrorMatchingInlineSnapshot(`"error during transform"`); }); }); + +describe('migrateRawDocsSafely', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('converts raw docs to saved objects', async () => { + const transform = jest.fn((doc: any) => [ + set(_.cloneDeep(doc), 'attributes.name', 'HOI!'), + ]); + const task = migrateRawDocsSafely( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [ + { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, + { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, + ] + ); + const result = (await task()) as Either.Right; + expect(result._tag).toEqual('Right'); + expect(result.right.processedDocs).toEqual([ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + { + _id: 'c:d', + _source: { type: 'c', c: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + ]); + + const obj1 = { + id: 'b', + type: 'a', + attributes: { name: 'AAA' }, + migrationVersion: {}, + references: [], + }; + const obj2 = { + id: 'd', + type: 'c', + attributes: { name: 'DDD' }, + migrationVersion: {}, + references: [], + }; + expect(transform).toHaveBeenCalledTimes(2); + expect(transform).toHaveBeenNthCalledWith(1, obj1); + expect(transform).toHaveBeenNthCalledWith(2, obj2); + }); + + test('returns a `left` tag when encountering a corrupt saved object document', async () => { + const transform = jest.fn((doc: any) => [ + set(_.cloneDeep(doc), 'attributes.name', 'TADA'), + ]); + const task = migrateRawDocsSafely( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [ + { _id: 'foo:b', _source: { type: 'a', a: { name: 'AAA' } } }, + { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, + ] + ); + const result = (await task()) as Either.Left; + expect(transform).toHaveBeenCalledTimes(1); + expect(result._tag).toEqual('Left'); + expect(Object.keys(result.left)).toEqual(['type', 'corruptDocumentIds', 'transformErrors']); + expect(result.left.corruptDocumentIds.length).toEqual(1); + expect(result.left.transformErrors.length).toEqual(0); + }); + + test('handles when one document is transformed into multiple documents', async () => { + const transform = jest.fn((doc: any) => [ + set(_.cloneDeep(doc), 'attributes.name', 'HOI!'), + { id: 'bar', type: 'foo', attributes: { name: 'baz' } }, + ]); + const task = migrateRawDocsSafely( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }] + ); + const result = (await task()) as Either.Right; + expect(result._tag).toEqual('Right'); + expect(result.right.processedDocs).toEqual([ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + { + _id: 'foo:bar', + _source: { type: 'foo', foo: { name: 'baz' }, references: [] }, + }, + ]); + + const obj = { + id: 'b', + type: 'a', + attributes: { name: 'AAA' }, + migrationVersion: {}, + references: [], + }; + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenCalledWith(obj); + }); + + test('instance of Either.left containing transform errors when the transform function throws a TransformSavedObjectDocument error', async () => { + const transform = jest.fn((doc: any) => { + throw new TransformSavedObjectDocumentError( + `${doc.id}`, + `${doc.type}`, + `${doc.namespace}`, + `${doc.type}1.2.3`, + JSON.stringify(doc), + new Error('error during transform') + ); + }); + const task = migrateRawDocsSafely( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }] // this is the raw doc + ); + const result = (await task()) as Either.Left; + expect(transform).toHaveBeenCalledTimes(1); + expect(result._tag).toEqual('Left'); + expect(result.left.corruptDocumentIds.length).toEqual(0); + expect(result.left.transformErrors.length).toEqual(1); + expect(result.left.transformErrors[0].err.message).toMatchInlineSnapshot(` + "Failed to transform document b. Transform: a1.2.3 + Doc: {\\"type\\":\\"a\\",\\"id\\":\\"b\\",\\"attributes\\":{\\"name\\":\\"AAA\\"},\\"references\\":[],\\"migrationVersion\\":{}}" + `); + }); + + test("instance of Either.left containing errors when the transform function throws an error that isn't a TransformSavedObjectDocument error", async () => { + const transform = jest.fn((doc: any) => { + throw new Error('error during transform'); + }); + const task = migrateRawDocsSafely( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }] // this is the raw doc + ); + const result = (await task()) as Either.Left; + expect(transform).toHaveBeenCalledTimes(1); + expect(result._tag).toEqual('Left'); + expect(result.left.corruptDocumentIds.length).toEqual(0); + expect(result.left.transformErrors.length).toEqual(1); + expect(result.left.transformErrors[0]).toMatchInlineSnapshot(` + Object { + "err": [Error: error during transform], + "rawId": "a:b", + } + `); + }); +}); diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts index 102ec81646a92..461ae1df6bc3d 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts @@ -9,13 +9,32 @@ /* * This file provides logic for migrating raw documents. */ - +import * as TaskEither from 'fp-ts/lib/TaskEither'; +import * as Either from 'fp-ts/lib/Either'; import { + SavedObjectSanitizedDoc, SavedObjectsRawDoc, SavedObjectsSerializer, SavedObjectUnsanitizedDoc, } from '../../serialization'; import { MigrateAndConvertFn } from './document_migrator'; +import { TransformSavedObjectDocumentError } from '.'; + +export interface DocumentsTransformFailed { + readonly type: string; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; +} +export interface DocumentsTransformSuccess { + readonly processedDocs: SavedObjectsRawDoc[]; +} +export interface TransformErrorObjects { + readonly rawId: string; + readonly err: TransformSavedObjectDocumentError | Error; +} +type MigrateFn = ( + doc: SavedObjectUnsanitizedDoc +) => Promise>>; /** * Error thrown when saved object migrations encounter a corrupt saved object. @@ -37,7 +56,6 @@ export class CorruptSavedObjectError extends Error { /** * Applies the specified migration function to every saved object document in the list * of raw docs. Any raw docs that are not valid saved objects will simply be passed through. - * * @param {TransformFn} migrateDoc * @param {SavedObjectsRawDoc[]} rawDocs * @returns {SavedObjectsRawDoc[]} @@ -52,15 +70,9 @@ export async function migrateRawDocs( for (const raw of rawDocs) { const options = { namespaceTreatment: 'lax' as const }; if (serializer.isRawSavedObject(raw, options)) { - const savedObject = serializer.rawToSavedObject(raw, options); - savedObject.migrationVersion = savedObject.migrationVersion || {}; + const savedObject = convertToRawAddMigrationVersion(raw, options, serializer); processedDocs.push( - ...(await migrateDocWithoutBlocking(savedObject)).map((attrs) => - serializer.savedObjectToRaw({ - references: [], - ...attrs, - }) - ) + ...(await migrateMapToRawDoc(migrateDocWithoutBlocking, savedObject, serializer)) ); } else { throw new CorruptSavedObjectError(raw._id); @@ -69,6 +81,58 @@ export async function migrateRawDocs( return processedDocs; } +/** + * Applies the specified migration function to every saved object document provided + * and converts the saved object to a raw document. + * Captures the ids and errors from any documents that are not valid saved objects or + * for which the transformation function failed. + * @returns {TaskEither.TaskEither} + */ +export function migrateRawDocsSafely( + serializer: SavedObjectsSerializer, + migrateDoc: MigrateAndConvertFn, + rawDocs: SavedObjectsRawDoc[] +): TaskEither.TaskEither { + return async () => { + const migrateDocNonBlocking = transformNonBlocking(migrateDoc); + const processedDocs: SavedObjectsRawDoc[] = []; + const transformErrors: TransformErrorObjects[] = []; + const corruptSavedObjectIds: string[] = []; + const options = { namespaceTreatment: 'lax' as const }; + for (const raw of rawDocs) { + if (serializer.isRawSavedObject(raw, options)) { + try { + const savedObject = convertToRawAddMigrationVersion(raw, options, serializer); + processedDocs.push( + ...(await migrateMapToRawDoc(migrateDocNonBlocking, savedObject, serializer)) + ); + } catch (err) { + if (err instanceof TransformSavedObjectDocumentError) { + // the doc id we get from the error is only the uuid part + // we use the original raw document _id instead + transformErrors.push({ + rawId: raw._id, + err, + }); + } else { + transformErrors.push({ rawId: raw._id, err }); // cases we haven't accounted for yet + } + } + } else { + corruptSavedObjectIds.push(raw._id); + } + } + if (corruptSavedObjectIds.length > 0 || transformErrors.length > 0) { + return Either.left({ + type: 'documents_transform_failed', + corruptDocumentIds: [...corruptSavedObjectIds], + transformErrors, + }); + } + return Either.right({ processedDocs }); + }; +} + /** * Migration transform functions are potentially CPU heavy e.g. doing decryption/encryption * or (de)/serializing large JSON payloads. @@ -92,3 +156,40 @@ function transformNonBlocking( }); }); } + +/** + * Applies the specified migration function to every saved object document provided + * and converts the saved object to a raw document + * @param {MigrateFn} transformNonBlocking + * @param {SavedObjectsRawDoc[]} rawDoc + * @returns {Promise} + */ +async function migrateMapToRawDoc( + migrateMethod: MigrateFn, + savedObject: SavedObjectSanitizedDoc, + serializer: SavedObjectsSerializer +): Promise { + return [...(await migrateMethod(savedObject))].map((attrs) => + serializer.savedObjectToRaw({ + references: [], + ...attrs, + }) + ); +} + +/** + * Sanitizes the raw saved object document + * @param {SavedObjectRawDoc} rawDoc + * @param options + * @param {SavedObjectsSerializer} serializer + * @returns {SavedObjectSanitizedDoc} + */ +function convertToRawAddMigrationVersion( + rawDoc: SavedObjectsRawDoc, + options: { namespaceTreatment: 'lax' }, + serializer: SavedObjectsSerializer +): SavedObjectSanitizedDoc { + const savedObject = serializer.rawToSavedObject(rawDoc, options); + savedObject.migrationVersion = savedObject.migrationVersion || {}; + return savedObject; +} diff --git a/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.test.ts b/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.test.ts new file mode 100644 index 0000000000000..80c670edd39ba --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { TransformSavedObjectDocumentError } from './transform_saved_object_document_error'; + +describe('TransformSavedObjectDocumentError', () => { + it('is a special error', () => { + const originalError = new Error('Dang diggity!'); + const err = new TransformSavedObjectDocumentError( + 'id', + 'type', + 'namespace', + 'failedTransform', + 'failedDoc', + originalError + ); + expect(err).toBeInstanceOf(TransformSavedObjectDocumentError); + expect(err.id).toEqual('id'); + expect(err.namespace).toEqual('namespace'); + expect(err.stack).not.toBeNull(); + }); + it('constructs an special error message', () => { + const originalError = new Error('Dang diggity!'); + const err = new TransformSavedObjectDocumentError( + 'id', + 'type', + 'namespace', + 'failedTransform', + 'failedDoc', + originalError + ); + expect(err.message).toMatchInlineSnapshot( + ` + "Failed to transform document id. Transform: failedTransform + Doc: failedDoc" + ` + ); + }); + it('handles undefined namespace', () => { + const originalError = new Error('Dang diggity!'); + const err = new TransformSavedObjectDocumentError( + 'id', + 'type', + undefined, + 'failedTransform', + 'failedDoc', + originalError + ); + expect(err.message).toMatchInlineSnapshot( + ` + "Failed to transform document id. Transform: failedTransform + Doc: failedDoc" + ` + ); + }); +}); diff --git a/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.ts b/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.ts new file mode 100644 index 0000000000000..6a6f87ea1eeb2 --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Error thrown when saved object migrations encounter a transformation error. + * Transformation errors happen when a transform function throws an error for an unsanitized saved object + * The id (doc.id) reported in this error class is just the uuid part and doesn't tell users what the full elasticsearch id is. + * in order to convert the id to the serialized version further upstream using serializer.generateRawId, we need to provide the following items: + * - namespace: doc.namespace, + * - type: doc.type, + * - id: doc.id, + * The new error class helps with v2 migrations. + * For backward compatibility with v1 migrations, the error message is the same as what was previously thrown as a plain error + */ + +export class TransformSavedObjectDocumentError extends Error { + constructor( + public readonly id: string, + public readonly type: string, + public readonly namespace: string | undefined, + public readonly failedTransform: string, // created by document_migrator wrapWithTry as `${type.name}:${version}`; + public readonly failedDoc: string, + public readonly originalError: Error + ) { + super(`Failed to transform document ${id}. Transform: ${failedTransform}\nDoc: ${failedDoc}`); + } +} diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index e09284b49c86e..f74fe7e7a6e1c 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -35,7 +35,7 @@ import { SavedObjectsMigrationConfigType } from '../../saved_objects_config'; import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectsType } from '../../types'; import { runResilientMigrator } from '../../migrationsv2'; -import { migrateRawDocs } from '../core/migrate_raw_docs'; +import { migrateRawDocsSafely } from '../core/migrate_raw_docs'; export interface KibanaMigratorOptions { client: ElasticsearchClient; @@ -135,7 +135,6 @@ export class KibanaMigrator { if (!rerun) { this.status$.next({ status: 'running' }); } - this.migrationResult = this.runMigrationsInternal().then((result) => { // Similar to above, don't publish status updates when rerunning in CI. if (!rerun) { @@ -185,7 +184,11 @@ export class KibanaMigrator { logger: this.log, preMigrationScript: indexMap[index].script, transformRawDocs: (rawDocs: SavedObjectsRawDoc[]) => - migrateRawDocs(this.serializer, this.documentMigrator.migrateAndConvert, rawDocs), + migrateRawDocsSafely( + this.serializer, + this.documentMigrator.migrateAndConvert, + rawDocs + ), migrationVersionPerType: this.documentMigrator.migrationVersion, indexPrefix: index, migrationsConfig: this.soMigrationsConfig, diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts index ba6aafbb2f651..df74a4e1282e4 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts @@ -129,18 +129,6 @@ describe('actions', () => { }); }); - describe('transformDocs', () => { - it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.transformDocs(client, () => Promise.resolve([]), [], 'my_index', false); - try { - await task(); - } catch (e) { - /** ignore */ - } - expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); - }); - }); - describe('reindex', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { const task = Actions.reindex( diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index 79261aecf675c..d0623de51e4c3 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -22,6 +22,10 @@ import { catchRetryableEsClientErrors, RetryableEsClientError, } from './catch_retryable_es_client_errors'; +import { + DocumentsTransformFailed, + DocumentsTransformSuccess, +} from '../../migrations/core/migrate_raw_docs'; export type { RetryableEsClientError }; /** @@ -46,6 +50,7 @@ export interface ActionErrorTypeMap { incompatible_mapping_exception: IncompatibleMappingException; alias_not_found_exception: AliasNotFound; remove_index_not_a_concrete_index: RemoveIndexNotAConcreteIndex; + documents_transform_failed: DocumentsTransformFailed; } /** @@ -523,28 +528,13 @@ export const closePit = ( }; /* - * Transform outdated docs and write them to the index. + * Transform outdated docs * */ export const transformDocs = ( - client: ElasticsearchClient, transformRawDocs: TransformRawDocs, - outdatedDocuments: SavedObjectsRawDoc[], - index: string, - // used for testing purposes only - refresh: estypes.Refresh -): TaskEither.TaskEither< - RetryableEsClientError | IndexNotFound | TargetIndexHadWriteBlock, - 'bulk_index_succeeded' -> => - pipe( - TaskEither.tryCatch( - () => transformRawDocs(outdatedDocuments), - (e) => { - throw e; - } - ), - TaskEither.chain((docs) => bulkOverwriteTransformedDocuments(client, index, docs, refresh)) - ); + outdatedDocuments: SavedObjectsRawDoc[] +): TaskEither.TaskEither => + transformRawDocs(outdatedDocuments); /** @internal */ export interface ReindexResponse { @@ -747,8 +737,6 @@ export const waitForPickupUpdatedMappingsTask = flow( } ) ); - -/** @internal */ export interface AliasNotFound { type: 'alias_not_found_exception'; } diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 832d322037465..d0158a4c68f24 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -41,6 +41,8 @@ import { import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { DocumentsTransformFailed, DocumentsTransformSuccess } from '../../migrations/core'; +import { TaskEither } from 'fp-ts/lib/TaskEither'; const { startES } = kbnTestServer.createTestServers({ adjustTimeout: (t: number) => jest.setTimeout(t), @@ -1014,41 +1016,30 @@ describe('migration actions', () => { }); describe('transformDocs', () => { - it('applies "transformRawDocs" and writes result into an index', async () => { - const index = 'transform_docs_index'; + it('applies "transformRawDocs" and returns the transformed documents', async () => { const originalDocs = [ { _id: 'foo:1', _source: { type: 'dashboard', value: 1 } }, { _id: 'foo:2', _source: { type: 'dashboard', value: 2 } }, ]; - const createIndexTask = createIndex(client, index, { - dynamic: true, - properties: {}, - }); - await createIndexTask(); - - async function tranformRawDocs(docs: SavedObjectsRawDoc[]): Promise { - for (const doc of docs) { - doc._source.value += 1; - } - return docs; + function innerTransformRawDocs( + docs: SavedObjectsRawDoc[] + ): TaskEither { + return async () => { + const processedDocs: SavedObjectsRawDoc[] = []; + for (const doc of docs) { + doc._source.value += 1; + processedDocs.push(doc); + } + return Either.right({ processedDocs }); + }; } + const transformTask = transformDocs(innerTransformRawDocs, originalDocs); - const transformTask = transformDocs(client, tranformRawDocs, originalDocs, index, 'wait_for'); - - const result = (await transformTask()) as Either.Right<'bulk_index_succeeded'>; - - expect(result.right).toBe('bulk_index_succeeded'); - - const { body } = await client.search<{ value: number }>({ - index, - }); - const hits = body.hits.hits; - - const foo1 = hits.find((h) => h._id === 'foo:1'); - expect(foo1?._source?.value).toBe(2); - - const foo2 = hits.find((h) => h._id === 'foo:2'); + const resultsWithProcessDocs = ((await transformTask()) as Either.Right) + .right.processedDocs; + expect(resultsWithProcessDocs.length).toEqual(2); + const foo2 = resultsWithProcessDocs.find((h) => h._id === 'foo:2'); expect(foo2?._source?.value).toBe(3); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip new file mode 100644 index 0000000000000000000000000000000000000000..726df7782cda3f4afdae7b214eaa482ece1fd725 GIT binary patch literal 444632 zcmd43W3Xl2k}kYi+cv7UZQHhO+qP}nwr$t8ZQJ%&@9Ep8`=0xri0*&iipUk~$C_iN zW3B{@<5F|L5g84vu<`M*p!&Tz!&mz~8U(*J|JZf33oS>OO<{=LRvr z003zJvWkO|iItJHqk|5;`9IY9Yr((dU({;o$tIYR%S_MCNLP`c=p95)g~}`ef+*u*7!J_siZfc6chBZ!9zFDKLYk2 zu10Vuk(H}N8yFk?!_5e_NxFZ@-?*9jN+CTnJ;mI7Co@MQ zRwq4W?~lt9)6^#bK+fX3rd3(9%NVy;-j?s`bAJJYMQE#wLjZ%E@R5dy@%D9Uk{$yn z+N1bz4(dWBxB_>@OO|#Tpek}B8_I(!+VUvQ017FHd($g^@hEP=6+)QDC(tO`Qm0FS z%D$?|y1^;FZXk!&HXtZ|%>YJ?(&SnO4~El-{5Km#tV`tQ0NxBW{@7^sM=<`yMk_sQ zGh-tMM=Dxs2giR{`lB>S_b>SyOQ+N|<5lCw{$vw)zjC@}Wlwv8L`1m!z4$+Z04FIn zMMm|beyW{Xjy1pOD|0g1d|Dvsq(|-lzkBSI?ujcOfI$XMwJ93h-Sjy zHWYa`13(W)JxU`kWj8K4DMsczPBSJ&IXgZnBX($S4+9$p2W#u(q{o^|_X-M({Ly~= z4e{Ud<}a@rGrr|=g8cC+*xx+KbY_1N$KfAtmHKEANOip$`dtXo5YWjno z5qAqHI86%L1Hkvnt}1$+h|7&8wSS@++q@iCU+b!uITaaDF@mC?C)lBn+BQP1!B!i= z-ZI44EB4KmByunLNW5H(H4dj4#Vas_1)?NXLq5shrcH_*Ib7RWtuj+|gfy(ax`!F_ zu8z`y(~f4FxrIZIcppBP+cBq#7Xm(`69Q_@s`LEH2RjdQ_T_-@$&hKFRr5zJIKa@LaLtY;clMH$Ii(h^IV%pK=rt z2lZSVrDm{bNt(}*gQ|m{PVjBYD(XgtfnbtrOBS9mg}XwF?NOoHu}{%l0qJr27fipt ze~b=)nVR;l`?Wv9yU{1HnVK#n%M*~3mKQM~xk4kAE=eN@1vMrsYB2&}2A+x`!Qfee zEE?0NeNcz>d|Y%c5?N$Xn;l(#fh*8jIvv+<*`mbJ{hWi%E3pgI*an7KuMin<9s*+$ z03AeXN{P38XR0;~o-|d&FmfyYO%82AeGq#ZSX-mrI2Q2E!+*q3=MsOfA*qO*YOLqh zxbrantb&8Ft^cZWoPX^9gaz5P>r1R=aP1sAm)Z1Nqh?l{k;vAAN z=aYB?V&u-fl-7@VP>VFOp;59mf^u9L`A9ctnVX`TVr5oM zs-|`!+Tj8l!bBy0TJUulOZoWvxv53*3+(?c?|LvrdXabl0JcPbSBNbBS&05keQ$Di zQcT=jdxjQpfxt(c!`#kq6o2=UTev`cdT#rZGpnBSU6iZ1rg* z^H|cA%M9)3=(~m&xdST-u8CmmRRQv$g4X&y)4a0?0PuZA^TP+@IE6>vjU=_HfEycV zHl#NTAslw;_qy7|_b|dRd3wd1fj8R$L+vlgad9Vh-Q}NSw!CxQg&Cq z#HNMJ*G}Zf3tCcN+*mjO;K9=yiz-}bT=tpcnzR!-2DZCGl0B6#R{hYd1jJvT#?F=ZWBG+1*z|ptZA%!7p>@MHLX8?R`$x)f+9xR zHrjT7)(7-b3AVYlxly^gy;AdU;@SoRL$hO?Lp#qpv{6-pLgdzxzyk7pRnwF8QlVZP z00{Gn0@Op;qK6~g08ayj+c)#B1ne?{6TkWDf*?i)lJ;ui&d>_=;++b;U4uFV$cd(c z4i11qH`VzHXl7zp_JRMw+HOJiUOYc0Y#`yS;ilTQro|nkqhpkVV-(a~G~^=*1AxMf zAGPh1NX8Vvv5ALLc|UT60AGw=^-BSmsBOVkE|)yUZg1CspEP%9S-`MDqd<`_py>=i z_WULQL(pwQsj&TPTWo!|ej%{bCSc>_&jHzXfa+S;&R=%70K;C}bMRJW$uh%RLlq8K zk)~*1fnx(T1($9YgEZF$Z?5sjA<(ys=F7q||M2N~9)$*w#g7flKwa_baLVs&!6$ac zPzK)U5fjYRGGHPlw*bh01j}>i98^hE>mKYfQ;^XefHMqBB!9!#*x6+Wt2ZbI*<^tV zG7hi}-k@8IA;@%g*4sk>u|DEPWQ~7J5>&y?JD6^&ObzN<-1|vGVX${tQiOs!ut5#F zQRp70(re8Llz$^+PbtaD)Ym$IgyN6Di;r0mYpy0K1yS#fvEbRly{w|mMm21ksYgMt zky73`;YiJ>KHHxfa+XvyQVUs_?0~zf>eIvw$DY0l03cqcey4erz~$&5_5ml|@#QYi}17BVFk7 zzSS;<&pht_=VMG_V5`O0Y{!HJAo{zTnx{p11ojZ`;Re6Xeq(#R7D-?)~wsFWTG)hnB4uiQ5 z2n8C+30*K7rP=$|zVodZ0S-O+^qq!Il|QEV=72Gd#e9C{>*<6dmYj@t;f80r`C@V4 z&NF1q1o2@T-52$ZEX&k$cD=DoJpeA=JtKH6D8?yR4Ocx5`Fv|&msKi z=7AZcUnV_dlpIQ=49UWg0Yh-9Ta(nPJ6YJT8XDw&C zIIVn|ghYV8^k^xki2%6j5IX?wc5WD69^qMiwlYu$HRH#X=bW>E=6*MU>KY&1IX!uP zt>8Oc47CCG?KgLBW~TyKQI3pyv>LT22X*hSMvU0b6>bq*>jKtL9j(xOuK2O=Cc~k6t7CHDI$6 zp||q}>=Xo~?;aZW*qi2qs!{&u`bF8%2$s&@M@Pb+y{g}e!Y`^KFO$JOhhdssJK}X+ zfp|kv(%jbtmBLT6A78EJvx|EtA17PQv+s?-_kxfD8*p0V`OQAL#XKwB(=!tAH*BkB z5T@kQobyUpQnRCb60(T}D5}`4;=_GwNJlaeWJr%)drO(Fb+-U9c7eemlJ-@u*9eJ2 z#>*MxN1uAwa!?3oroqYNlLokB2ia1nZG`@Q_A}&kAn1o~Ec}VGXIk_|yPRiUO;wAI zo7Jx_a19HC5Rfj)NKG#+*~Wam+vMq+b7!rR2-#LM=vq(i26f^2R41B2BFR@}tF_qrKuP_x zAHkeib=WQ$)k<&t6Kc1S(6<{Hnq?;rsNKM{-O(~OBh3c(k3NJ224< zK%9<+J9C$(dWzrmT9|HdVP!L@MP zo)v&FX7H{nZy?)k!-Cd^tjbsRIpycn3#}&AHt#Rx?<6W_t2Cc1u&cU6nQl1L6RVBB zKj9UnGdayD*13`Bd#SNG_%z1s8J%w*HE{zamVP~hPL=v?^@Wbt1HCwUqj>q&FKk<7 zBVc($Pii%pRDhenD(1vj>JkB|Gc6_MYFz|#xw2U;EmlPlE*CgQlg?Yc81O_9t&u_< z<31w+7=?3INtREw!39?k?Ya+-+)ONib+k*nOhx1!zIP96KEPf>V{|_TKt|(JVWZ>o zwCymCS2`==@-l^(8p*(b*v}<9^KUoh^;$mTYpFMqmVNHoUxX%j1{{*CZYM~-y)7ANr$CIeS%FQCvr9; z6*wp`L}FViF5?{oHIrWUgT=4aA!%4q^)e*gWGN>4!K=3fQr6tUmKhA%J(On+@TUWc z>@FE5srDrIIC9E$#D(-^nFHLE4>-UK9jNZZkz{5NCIH){{lRzyRWE0|v5Qb2Nm&ie zFqF$3=KFxz7Z$LUMX6{t6UPVjE)I>vz$g&`aEOjGF$&XpyDVezINDy4=I%IKo|`H8 ziVHXk=Pgw0)$CR6pt9&u;|g(6f(e|y7FVO`{A66)sO@QpVjrnz zI-e>J8BP05S}eh4rFPW&WHLH+wX{lvHb|ebQSq#kIX;yn`RUTjuNM~tc`GH?sfSo6 zP3uYuB9X}UN)%!c0K>)Qw!taUYJlt$F9?s?dc{v0Aju}v#~cnbR$@NTzr=zql;4`CImxha_)UAh9rN5fg9?(2op(eb|v1+ z2I0Q|N=*GaHQ<(?>Ii=>$R8%!H$M?D5j=+Kz&kQ}h*SBrV6P5mWxZEQA(Exi)L(~H z<|l#Kr~n%-JQ>0|2auc6)y75*f&ZYkejXDhX_Z<0>O=1KQnh4!?V`<#6*uBe;%D|d zzkV!$^}}nMAeLW{^Z!kgIjbdLQ9lOI#7jvhUS41M0$!}WoiE>L0%FUTu$;&XnX5b> zaI!4;BBqD%iJKkkk)91Y@Dgp3tElYz;YA6H08a#dnR!zh{4u~7yaRYoKaUvu>s{NU zc?>@M%T15OdmVWBHTSvhA?jdj%=}u5%B0HA@PfG=~;IsgS%crLt8{eCOGuvsr}mV<0c2y>)J`IU-O z?U&T544%voxJT$%t-lVF7*a&V^KuEc@lVTCDH4f;lcfa8+|YO4aPyQ{RK#GKdLAEd z&HU1?kx;fTPs3QZGC%r6*@3hftunS)O4itTt_ib6u598*0hL~J<>C?(z;3o`L7iAr z#Vh;61fiwPyR_eU>Yjjn(qg-+Y38fk&W52sYMGDei}K?N4Xqvw$OO2QE{c!uf|5x_$1Q${2j^&%U)8lS4mpl4etax?h+(VkT*{rho~0bU{$G9r=MlH_^Z9eIHkB_b#xHi}uh9~(vx;|w1!g-Eo1nvsoL z;qfe+<{B$jR++bU)N4i_U{5rF8`w4lzf9B7dYMgNSuTB;&k@$<_BUjfTbPXWV1P%g zSOAR{6QERwW1{MS2ESY|hZ{9X!xa^jC5kGJT7-|`D>P%n`ySV^=#(PSyJ<-W#y4jK z2!f18J_?2OLm<7>zPX4!uN9JMa|;*vO<1qhR@3RM$^3D=vcQT}I$y?FAPn$$GHdc3 zc`xm9-mpjxMP#;DbTkuf5qOO7Z(?v_ix42TYZ8V%4_R0JeM$^n?D8JbqRKZwi)W!W znL9cZ^0yZG_X+jEm+e@(*!9@gdDYERRTfywi{uN~pFcOZv>f$h(rU;ho^~LSboVQo zbP8Y4nVl}Vc(q=P0>S+06&SFguINThjJRZUF$Nj>xUSY+uDp$H>Uip-b35-h-ZIt2 zPc}@n%&$Of+?%pi4=D`K-@v*Cvh&#-Ld^ngKixGm%d<6Ycn{dqSSa>N%^8X_jki>$ zyXr5Scq?cM2(rwspn7mN_2Qt5k+*Z2IC-3P2XAc_g|3{}!JZQ|EZ1M>XQYBSVqn=+ zN3#-c5Duuc=U`#Y${=~X-?21;dO-dvLn_2LO)Tgb%rs_6Z9*`(b`P>6)gdV@xU&^{EdkW3<6CqPRCB@9UOqjMN$M`UcAy4BOf(~rqCokC{XnBz*8!2FQYOEefI9Y(!?1HN zzmm>a>Ol*yzGTz8Xi9G&>mgiBlEH`DQw`jF~#c%=B zkxU@EY$X}MdiE~1RNRi~dvTY^B?xn!>}$C){07Qh@i^qT^XHTbb0lddH&HM)ZVg~+ zgdj>XAyp2}64lBPN~iZ&4je8AbisovMkDAR_2Z`}Al+xMio(u}op^!kmr?Qd)w1L) z&W_&=Uker)mz9m9re3J6e>bDSVS1^yiAB5MJmoqBz*fI_&3WY;pr*O+J%Z-8G0IZ! z^+(TPmCRj;JHbgh57QlkJ8O1K8dm;jS60GNVDI$Ksnwj5!6rn{dYl}*U}**rpia@O z^mO*veq!wwzJz1ubZ%DPN%!FDdg+A|-0kVSf=~avc&(m4>{u<8I{)%SszhWih$$nF zH&Zi0KHoAta=dK}nt?+lD0-3Ugsf_(5s$qHSfN@=#g7>587RsWjG9J0c zqdCEBFo=>S87w@ltz(eFBB=wN9MlSJZre@P$v7iIc47vK(LzgKi3&e$Y+<<*b2sj@ zF!?g1-_s-+4N#`@yG++?B%Y%(o^L@~6&vxCb@aX`Pi#k2W6sD3e=Ry=GS-*X|8YVX%(Wi*pA_QSM0O{8%pmojR4&I2;f= zJx+>uOV-HgZvHXCQSeIixJeO-39}l#);*$WUaX*>$0NlCieM~XakEH=w9V`4v~((i zbt>a?rf#Y~%EM(=oKx2*ibqj7$iPaCn)7phcg3{`j~;4~I{fMarB+H-ovRLHH}sN&Kyp%I{JnxlO$p2XRB z(~9Jz#Ex;=2pywof`15YYpz#00}pibfCwoNhTBf&jSnp0*{_4d^S-%NF#1Fr1!R-NYIL$gSUPDxxa1MGFXdxapT2$d42sFJFRIXU1A8brwoC|h24>eL#zU8ncp8YHq zA-xm7{Pxc?CHQZLA*awoUn1~J9Y$&Dvx=oqIobP< z6%H!fKY5U9p(j9N1dzB8uqM!;%SCP+ihNIO z90trj<<-7j4Q`rIWWY}GPbJQwBGH7%EJE_)8@uV)|gTSS)hDv}EkU%&Qe3>a(_ zmLw$_n;H_`o|JcPSrZr-sn(d@Wk67V(iOMtmM-KUv<~}w3q?8(fVJIghCNt1jGqdd zeTNAa8cbkwl>(<->SFXv5(&3MMn-ugyT}*AWce^|Acx2aGnv%_cFORoc^5(&V5*e#prCNKC@l#@WLumZ^H8mnHKM$Mc*5dk}vaSx-hg zAaZ!-(f6I|l}@c~08;R-aoC#ID1t;b^e&T3EmA*G2=zo}!0z_;sos9GvwmeH zl>9RHD#&zEH^*NKbt$)_3<9|=4uManm^cf!e8R%xiuAcuMl(2dT6+fDb{Fkjg%@O`@)vO?1sL>;}V4OEX<*$)-cm$<&UR2WIfhaI?#7!fv zp&@#_Y0b(^LB9g4cy3z%*1cyE+q_L=S33crC}`@(M1)^o(v_`CoB1o#0hh0ccsPy~ zAxwf-Y+GhO4edq=wP_eu_`}r4Ym-cuNdx4ux-j*0j8C}Zx^Aw<8*GbCcD;-i7bTK&wo`gO)L0LX7+au=bpJe%7C_6fyR)%=q`=C;Pco9 zl@yf``W)!T5hM)LyXdAdx>Zhp%or2|Z+4kc7S3VYgmM1Q%6oI2uLgFc0-7r|?7|Y9*y9 z=?E*FCS%0MyU}!1DyVw!*oW&1`#ewrEV-u0V#q-67ImT8kZn~twk|eQo$cAomN-6NW#MbOv*>VnEl9aGd?Bm#*j{#%}?0 zZ%k9{LV(%qtn(bqStyy4VzZrT5vhs7+WFBiG*{pcmI9?+!`tsU^JDTZHD`#)dMp^AeNT+_VnT+agtgUL_u#Q{!|{!P<6j_8ELL$+%^_H)SOC98HGxV+D6=j!lYO zPx+)Rw}YkL!-@Tl(#zMdQ+3Y69wMPswLx+cNnX!Czd(m8oofM7BPH1$n0fD#5&! zG;s;_#S7Q>efOX+I#QyK5)SStG~(d=3PK&R*RE!|>}J!N$j{3rV@$?L(Zj|pA>0v& z3+pkeLF~%lob}RfRgGemAnt-GBV2~1qwH!m+p5uKludY>zk5O!bNd8n%#e;Bt4>;W^K0HS6Dx{~ z3H9$i(2B69MeMg)j@-Dc-?U?@FF05p!96|`b;D>5}?Q(cL2UxsQM5AgacvoVg8ze2Y<2?!pj-a0X)$ zr4HS6GJ3PD7=8rQ?Zlj3aU6*tiuzHW*vhiWQ<+2DQv%xK!zOpA2S1o&-{$Fm_%8l^ zac>?meH<^r9-n@MeKGQef_-muzbG;IGz)_ELz*XP#0F=fo2%?&t{5Eeu_Fi7_2LD@ zZS@H^Z(x$OXN4!tH+rVI`Mui@DP$OX4#412`2Ohr_&0Y$kxaRvnu6naF(`K3%X0iF zVAwq=Hm26Cw{$tmgbWY#v~GgbBZ|%TPLFo1)AeNdr{!rV{RPbN^Q5O!F8aB$DTQD* zCVG=}&e<9csgJp<+_HiJ1w2PsgxZPq@m29KkCWG18X1O`+StAZmd zk?*Svxkg(P?~MB^r2tM12+Nt(c!Qs)a^^gnPA|6xUt+&QU>HAYhtM&3qOZ|FgoF&r z8Yuh<;X*#JrGsYS=C$}8a7cPN^#ja&ihe%1J9T-woStey?90 z=!_z$sJ7enm|^e-sm#;-p;`+madkNC(71uQ_A8-Q`Cl4b4F>J!})_>3cM1o%z;orc!0S?@E_FJ$d zZtSY=HRlLD%nYq}P7Kdh{&yu<`FGWGNa+`w z@E-;1f9`?$eAd`H%|Gh=7qLU?55ufvoGbS)-k$a!LHK`}iT{VT_ZKPg-w_l4N3^{Z zD_lBQQe%C=BLF%A0yG70x)2-vdVilRpyef`J7`@>g<#}ioH9Bt=}7-U&2`g_)tx6!|HCVCR*!jSpT4HwyIN)iF3Gj#Q> zav!B~S$F2K*MgnU6?1RGLSjCspb)g<`k4*W1rytbgHP9L5pjfLOoMg2zhi#;3@D$Q zvvm|MXJKzDP6ErqxTd84R2=zYX?U?kp%4v4hjE+m*qpE#C$DntzfQ8(>})K@ z+?(dNTZkF;6d;5aVv5x4d7JXsfK|9uqzh7^OApIZQvs@%8Jl3jV0*(rQOlpTZQiYo zEY8yNlx$wY7@=D00nuTg7to1>rs6K3d7*POKC-0>*XMxeRM-VuCha&$#nO@bP!B8f zKSUItB3_yrog6Gaj2H?1#llOAoCXQi+zaVCM8U63Hqt>VXQPtbDw-@qk3`s!WhMkc zIQ(O}aHATWo>NwQt&ot8`QjTKV>0kq@6tIf3~gYGqCzkljJYYKC=oZfCi0X8z|eqR zSpmzUY_(M_IbHqyLieJ)=viTpsn6?;xY$phj9y0O1R@?6y6?%j7YceL`b{%&9p)Y% zDRwJo8_BnnFfH_gZNDHBn#dPqkgGoDm@p1wI>fm003Q@=Xg!c9V$B%}pU7W-3TCAX zVR;8Ih?Z*-SUeCxRFte1-M%3VWp%ZUEwC^4UJr8)&JGhm#@;y|CkJdpI=DCxe=qm! zy+$5J$&fYHKa!-?tvam^5NlMPS~p8s(T9Dmig1kijLf7LnnZP!ce=Jwp&pQnj2?b* z5}0ELP$Gc7ZJltk4qtc3WpTznhh>8Ew^q3j19&RR)dHfP#~nE7>uX+@9(7Qd*d(7p z8R%>I8KFmV>OJl2N6^?6s7Qv5RwIXZm!;O8&ccnlTw0lft9gW*MNzdU+EYta@$&Qh zs05(Hkj+;SFzD?$%})9zXhLyEMMXnLPtuj|akLLDdkaw#vI^IA7E`W}OwYW@gZ4+m ze(LjE6Sv7%$>PeM z3Z2%@`zt&ZB|C(#r|!6eOv=vXo*Q?cqe-0et0EkXs!n^7vg;%FfdPd*be5;pN|mEu zQlbsRroi6YzhkLOJVQ)&bu^OAa2_qyIJC=pGMy-NpB_^zmuFGfMb>XplyHh1(kkk# z0?G_}BGG|CN#ciL#hhAX7Eb57(~u0LJKo$rWA`nMM=iZ-ExfyIO-wFGwyINvljELn ziuZV~*m|=td&j%CgSp?X#P_S;?-{yTJAH^alH701X>CZTaip-_W~yOJ;4R@|_-Gdp zVnB{6=Ha1(*f%)$^IhS{#-5i4)4*&g#rg*32oDp%2yTKoG5KqwQdeph^_tYjD2K=F zWekw4$gPVsvJNe90@+<**ZLFq>2K&EgP*{E@qttiamU#Kvc6HXj4D_XfHzB6B8@lI zMhN7|KDbG9mRyEyu^>BNzWod7@ama6BjisRZTMUEv-v;E=)Yw@TUjC*VfR?H_yaRV zcD2&ZLI`Iywp}#=A1^hPEhTl?C>6A2y2x}GTy9=wb1d!kB98p-Pu0r>#uql!k(8Wa z{m@M%FzCx|B_)P^9>Qgvj8z32u5#LQpkW$vx*7Gcf8MzN(8<~UJ}fT=YE%1__5+)g z2-{P3$0*uP9x({dozmu8a&0Uvsz!&sN!9K)hPe__?AUy+Sso1v2-?vB8r%xB+}|#Y z|6rov$6)Vlg5R%04Lp9Gx^nR5K*S095)(2N6U_D7&0>ZaoDU*9?|QSlazw3SR4t4= z;o~DuPQ?#uh>29)RG+Dtv66rzty8u`jN4M%11rV3SY3LLg z61I~(YbiV4J))0i5NU%%E&)(Oin|X`R{1FEj!2rpoL&{L1rrCES6s81uXz^ighf8A z#0DYy8;`-Uj!NEV#ypPW6V-8|oo~VlhH_IwgOKs@p2S(y?%uNC#wwCB(CO*>Le?Tj zq5_DX_3wiTYAe4Gl7NY=NQXghyx10ki6??!cLKC_{pJNk?Lr%lF6C7vRofGTMF{U)1u`$R4D*Z~_FU@MG#r_$Kp^Ha^ z>IN^k1B}08DqF$$Gb~c{w`$IjgjvhdLXo{_Lg<>)MPo#M;u^Rh3d(I4`;D#l>`V6i zVIM4$che<1#7DZsVs8N&)KMCFz2c0-N6_$5f}os>1%_s zo{4kd2H=U~0fOfIpYtr&SQTXr>h95KHC%PALp~o5CA)W17T1x};T7N3?{3el8fe{F zC#0!uh)u`yPg5&5O)sxi&CA`Hop1H*m60p_HR_50!+W3pzkiU?Ns5aPOvTXTe#FsE z%pUZd?&wnTPqesWkP5N5a1o#}X{^FOEX^}FS558fgg!@b#tVTbP^J&mz5GHCMg+G; zk6pp#Rl-0PjobUcU7~RKiM>5!6*{s%>ZLNx>|ObwrG2KhcW~Mvi6~Rwge2m=B||rz zNlCS7l#L;BvtVR6(Pt;XV!Tn9oIt0wN%ZchJ-8n}or%mN#eyXMnjp1g1CiuRbnVfS z`Eif)U;AxH+Y))=Te+G=Pyhf6h`-w!(*Coi8I&jK{w4o1DAT0m^{1wx`_yb=Gs3f= z2hh!2`i8D8S+qExgIh<^rAj_gk@xL8KRa`^)@*8xK*E;Oa)bP^1ZJXTkQ0O!29MW8 zZl(+bA;8I)h4A+m`~Xi4$I-ha?sl;fvjhIRv%0EwxxW9%uGzltbo1JHIY6X=0X@Mr zj_4$G2Q7}3C-Yr0DATAOifUzcnq(3Aq16?@!TT#-8b;#SFexm246$+v4bbbgxny1v|KpTtx4Xm zdOOkIU)5$msAwJ2W{g605#(k#dC=?gGp72VK0K_3OANY(RjhvG^wGs1*O7!;*UxGFcIGq{dK)iLx!YRtd#rpi0Y|8etdcDC0&bp<_4V1#gx97(O;T!~ow zs7*f_LTfa#blr>|2oxis#dO=T-ELGHSc&m8<(Q92jlBzbAkKZ-dcjU9`^4K2P*|8SDh_&+BpeN*G&BFK7D2UzIT`gm0F{5dGnP`Z5LPzL`dU6MQ zaG9pZhcdv(D=xl~hxQ zGSWhQolujGk_?ZE^UX~trf8_drH!P<$*87wfXoBNF;6hk?fe;$QwWbxZ!ps_FjCPm zQq@#bG1cfBMzD@fiAsv<{Z95INn29?^kjJd7d;soZfx@Z)|2_mt&fRQEd(5O0f*>BVAw`?dt zz4HP;Q8RaG73KtRV!`L*_g5#QbT0uBQS<5I^=CQgJJmSJhR^*2#r#4i_?Dm6tysM5Aa??5i*qYqP&23 zJ3&Z^MZq7lKo*Uvq14_>pGV#llsOC$8CKU5bmkX-*?zD)th$mQweOm_pIq_Z6*R1mR{^6vW(Z^K@1N1kNZ zEKxHU)+k+q#i;4g(g~*~tT9m<(qjh;n^-(H8ECrJ#jfgad7z&G9m!-5DzEfW^o{h9 zWAILLnYp}eAxB)&5388DNvZ0rrrBs1`>?Rmx~@q&1$!Ix=3weLatFE!d^V~`=7Xz56f+1de3K`|Dk!fY895(WX&dX zxf3LED|0^k^LTKzZt1SfKtm=JO@j*Bp21d zZ)qg4Zscv^*ct}si*$#t*9S;x9vh^o>D2-^RDLPTnM2F70y zVf+UoP#XRL5iCjnONhwQccA=l5b@X9%uh~)R_YHt9Q}E9%K!7;3_ACJLd3s2n<;zA zBAH-rOMOV*x~m5&Tr5yG4Vlj!a*X2Qh5=fJ^L0yCUHyN|-BWaA@%t|LBo*7XoeDa( zZQHifv2AwHNjkRGv2EM7olO7E;GCKHpINik+|0$k->dejT~EF1eLi2?`mEZdO@wwl zctvhiP%-pyFm0r!I7L`Bq)s%RrX{fB*ecSdY(?=32%YXyR!Br%sh+JXNP5DVFE<-rh)9N6 z6A2EAurf4(M|?*qL&*43mR8BN#NRRzwiCF4kNdN@r-9aq%h+^wW0K~Myf_w;>M|Ms zhF5+7@t?44m2QQVFq5_9xIyp_Z;$GkEV#l9%0$R19RSJn=t9Yo=v)_nf>QRzXJ=RTincg7(K2M{uU@e?Sb0UwEHSf`R$y4`gs(%45T*;ay?fGDezT z!?`fhy*8-%9-ibd(DGQPcRqBvP05}Yykve4Q?VAtQ)Ki)H(G3&CZ4Dgtei(yMg(Kd z1q=dp!LOKQS!D0vKK25dV1D3)XaW+w*1#G}Uf%ZtuCt>0)9PT@;|ut{2<1}z={_2H z`yvMCA~5wHXKe54ubv(kXfrAlb^`gVQC}sA8UboVa5+LJ=^e_un7M zkb34?-Wy+|pPYU9KxWRd+g_9|RwrLWO=Nsj@NgQn-N5`qLTEapA-Ppby6QYdRD~SH z=-4Br2Bfh-l!W{$^~@SblzBNdYS04B(=JCtL&)5k#N0oV3TTmGbe|x>FAJ~S-y14& zjZ|FWeM2)^;9({a$Q}j8)~NYCXuX2p=e*2dMzARgedL%$#MH@)al5b~WG}?*kR1E) z^1p64AP9tI*#>V72bBc3@rC@ADYv~akPQ`~Z4N3?fnq~1pZOuL`O0lA@zuERo~BQr zRq{yql6G6(zIDsB4m#w=Q>RrPOpyr}3mMG}Hh+6b6uMU2Zvy|ecAU;sMFeHCf z%q4o;7*#_;(r&NGwTFvyxVR_ce~)_~jgMRM5YW68k$;~o*0)IYuc6$K(kJgNa?^Sy zTjQ{78Tv6%S@j%wZ;iWADC0&UXs1!_@9SW^7A+Un!f)@)_#rO0@58cW zZF%Q&?ctcV5=SL9JdbP4t*s_s!OhbRF$j3l^5pqeJYVQ-TvPRR)zVw74lF0K_PY>J#^6bEsbzf=-u52SetD0ji^E*lin_2Zjd{}c zE!*3ZzWC}lT9vW(y(m}6-oBcQV9@g~wkR#8l*>xM`7+C%*mZqy8$n~HCH`zY8Zy9m zxCk9mR2$eCq+`-JpJo-{*a-eMw!9cobief6x7B?-;<&v^riwXL9gZXm=jqbIP-;2u zW7;dDfe33xRi9HclsSGRrp!5d8UZsR?AA&3SyO0AeyW;pbnO}$<$Lw>C?!)1;|qP!3YQZX zv9xMVxPZlklInzrAfOD6Y+r7fnLwkO)uYIB9ul^| z-?D_(uBO1esyS+E@PBYppy~X|%tO%5!tbW!=cVc`oui`6)Ie&r`l@W5CtgOA!0zE#@Xk1tm+AtqyWKbHP6XHIS z{)1>Kv=mP${$r`M{#_XVm*o$82UD~EGoS_aUr!tUZ-Ca?-JOc^KC6l_-+w8WwfD}M zm5B}hdiA3Hi0ZTJ{*PqV3j#rf@5B8++}?j;E|!YI)c^FU|I?5Dx2OL9Ip%60{r@!P z;^Jnq{wL<1{hNdcpBl3x>t&BpBOwsq?<9TvSwL@@SjbK>Zu$7LF@V~+@sXm+NVU9^ z@UjD zC)#q;cUyzx9axe_gZ7HK#FOL@HZ=G(Hc-E3C@djncutbA5+U{=l%9So7Qvj#luW9j zWp#~S&Hyc`QLbf~-0p3zi%o8=%YIbGM!4^fROWXZ*{_7>wddz&7+bHSsm|ly?>bYR zcYj+twN9H*N-ol)L!$ziup?gdQk6BqKD0SaJ`}P~6<&S2e;(bdbxYC&xOqNatSr*W zG=~95R@JIT^wYH&%64D$F24W5Vb49oc%^1f`h|3`UP_GSlKa!04q7=-b#i@17v3IR zQyDPIzwCi&b#f-gBfcNhnz|Ut%=R*J71DzG^Se9?m2mtCJ50ciq^>@iG8|c7Z)$HC ztxmB+D{r<>XMZ2ZQ#j~<7PaU(R_QZZP4%G1W2EE+1SZJb${5*Fu7xAK=yP2S{$>om zFY~P0uJUB$SoYkgIJ36)Zn;QPuW~q9Jw%VD``ccFPZuIIdYTfvq7BM+f+a@>@~Emo zT=~KWCxm?X8|9?~dR?8#U^cA3p6MjDuO2st`bNrclQQBI(RH7qD zE>6k^BQrTvXOxQaI7C+GHjd-y)SJ7O9C1-^LZ$eKXaAN_;ywHNZ=jy2g~A4&FC$p?V|8*HV{*Jl>{4drWWBB7pg2f zm+Bp@Dp!|7f|h2ZGjmvJyyXyE{F1?y<;I*dH17?!z88hIfk~fHHrzh%dH5GSoMGfO z83;7}-GPPYJG-Di=tIe9-Cw|6pT_mDkKi**A$?a6!w86o(!Be$w?$&=pt?$S7pPw< zY7gN2K+!VYHJ`%4^sjBr*N}Sitt60u_oVqP83o4e96J{S>Ke#4-?JjAUyVv^LPo|JD~fzo^taYo@i4+9LQrq^vpQ2Jo&e>;f3lenorEN zDFU!n)@b4nH1(46e|MS?JN&>clzT^|P0XLW<>_$)nU3J4^~IE(gxM}N1DSR-97A{E z*E&Fv+zl=x?EBy|o1JzIFAp*45~jIyBJD;~Cujej1n?l=N$t)h4Y4&hb}ek}7_!E1 z3^?Phw8%int)p(@+Kt)8rQMTHy5QA?*$hR9eGs(u*%U+(&DX*KE>xJEw*t0Za2OMn zejvkena#$xg?73v2N%`kGHdUAMmkE~RrCr{(@Qfh!&N#V=2Y&u0yl*wr~>Ji8*j~4 z-k5&*{CLA|@8IHQLrX#oE`zBjXEJ>x5B97j!$;ggFnnY8w*I&7EYw7X{Cmobi_U0U zQ4wZLf6#$4c{DhKF$WqjyCmH~ioPViR#e*{qMFLbQCk=%2%F3@Gu?;dzMY_RyC--c znsy*K_9cfe6*LA2lx)pCAaL3o%tprOK2hn=KO{`5u@XCwy6mZDB#OIqB!9U!eN#q$ z^3I-SMdOUupSTq~0ZAc*_S72>oD2ZfJ&eFc`6e9U<^xQu2l&d&zj@yhAhDAy@I$=z z9f=0q&*WJ_`P>lznP*94gl4H8Hz1at^h*NWSZO58`;H=L+s#2$;7u`8_E25B2I>Yu zc#O8bdTb0mAjvqK-ocV&QXJ(zW9H!}XFuHy8r?*~58R^d9Ry)*-lJ6vS3$jL1zEwN zvVSWQqtY`s?sm3j;ip-dPBL$U|dR#9L>`nLvSAV3!rAVyoje2CWA?2vc+O#)B? zM1U5xDVlMyiImx3S{M5y(MDpZoN+IESjS-THugYrlWx6tNyiZenk~uF2!%4w#5$yk z#BDtIGUNfntGJQGLEAH-KFTX&Svy|JIY&A;qPl)R-Ol<7t~XwX%k$Pa4tV5wBCQI= zFXlpxC+7RmDAAEp>5zvrQOUM@a_|8|(Mx1L+aR%*lxeIKR3IN{TJmCu7Shf3MWaSW zc0oh0Y83J6W5(#7uCQ}CKS+`vZ{$IQ0k&83Z|o&UE3z`f${m)Fd(M`v>p-&$=014T zJN)5mi1|k!8G-7|5w0r7xK(TV`*x;$^`t&@DGW!_vnQl1mn(R6}Ki;!VRhj9fRHj2~vq8HgKtFkCI2y)H6O;(W6C zGi^An_`5hehIN6-pf#97B6n?~JbebzeVwyHt4u~rx^470OKc1w#4MncFYd-^+{0`o zR){yOa!0Nz#_l1^MKSh(`Yq^3@>T@aJgT*~&x##-ah+t6=&p?y;nFU6PA+t38swSr z( zpQ(GVc~LJ#oXf<#>%DA5Necbso_}_6rG-fczm}rNIis0z7DqL0Nvy2lMo+E4r+kQ{ z;K-a5r8nS_;MPx#?fd)*A$J4j`H;47BdTD0Z2U#dM)>)Z07h06ri2q9Ppn`R3EY}s z9sl&*{c*9a2tB?={G;pVV_{g~cV(ktW)Zg=s{DaqsJC=pdlCy&3tw`q3I{D7f z?MXwTkbLs9OwTbwXT5FUpd-;SobbAkz}S$^o)moOvhI)_LnvwFnhzGMvjUk9?w3qQ zc%6#^VG4}$w}k*6kQ?v6r4ogC(@!uW{5_7+Q?cX`f&u5ce6sLn7oDwN382B$d59=} zJnQVl;lhD4b~C^bI+@p?-5^IF$z!VS1G&IgVF^-f>E2LhC@U*Sc=%8yKS>e=SYB4# zTNyoQhE~WqBS-A{3O%PbelVN@{3~dm0IYXFh8v1M>>Es}wS19co$@MDeA-}|n5rWiH zZJ=f>9NCCS9h*NG=#dx`xIWs5&&dmX(e0`p_R3FK2UUwsjR(xMzF6r+B zFY%1sx6kUwNu{*v!fN6f?;|lU|AMYo#COOF_kq2mWX@4d z7&A5FO}t|kFFaKc4;W`M+ zCh`c(S9}?z2O5q@0EqJ@eAFC__zsTX1PFpEgCBeRp~~ME^A1>eIiqS1E(X$aR>SN7 zR|yBK=M6tbdhZSS!Ny})q`qGPt+@V)(51m|SKucgUuuS3Q)!4lQ=j0(L3#9hb1~k^ zz7WJdzMf$`(3XP`!u$Ng_8dV{<(MIL%q>Tt{z1uG?3FzwTLVSc*bvWo)smWfgs%B{ zh>T(RC>o+z$62vO5nf{ou@@xS1!PUcCe43XUtnas@3;U^0rA)ftE%YyX0+$mh%^eReZ#4V?2l3kcZDTOvT?9m^L|62teS#QqNkso#j*emr zvEdftPk|c15&Y4vRI-xb)@R@;=|uw>#bAW=x&za1NUisz-nC;QxyqrJuTfaIPys5D zBZH(sC!xsV%@?Est{;h$w9}yv)$jSmc>BpY8BG@a8KjYvI!UKcQlXhg0OP+{MXE6l zVQ@ZM@d)f=80FP!llOCq74`!yN_qEARooY98}$v#r>O6C`4@?Hm!;l%?1SeG z5CH6sEhpMp0Cj+<9@?^6uPH7hwBDc~RJF}&4IXN_YZ*io&4!;7!fQrsnp!nIGAwv3 z?3PgnnJXcqy-igyr|{{MA{6bdqGF$EV!x^msf5&)VfcfYkG;nq8C5YK8bSd#C*z6d z0s3ilCHYpcO+O4h2<2{_KT&n~(ugA?CRk_+`LP&EsczvsqEU>b{L1j*rmKj@zf9~4 zNyugE^zI@u8mGdc{n54G_>!huAb0`#qsv#)fp8x*=<%dYkzIxv+~l;aIXx!gyh%vy z0HFhRGX=_3hOkl_4I~j`%F!6u0hSK19lV4p^By3s)NCvZJAj^w#V|Um3}*}Wf~iLn zEOr>ByuV}z>JOZ+eYhkJwJmXjf(~fnVs!I({MrdR>P@v^duc0T5)N5SvPlVzwWy=w zyAemZbqfcnMn6Zjb}y^Tju0)rS;~d}GKy?v$;Qn`DbX*nTh~S?;q;vwqXP%os~H#Y zxZDAs+x7$=_vGe`uvbLKeq=eZ3e1!u`9u~H$cprz=dJA6!suz8L_Kjr){1j^m&i{d zh@DW3dPU*OLjBknnL58hWIcClRfLIXm9go-$II9}p^T#HvrdW^qrGmD$*fCI;s1Dx zmGHt%{jAnr!NY;l=ZG7jj<}Xi8`qvHu*r$N`UbGaf$5CKo8bPAV#nJ;nDYy zr6jy5vxcNZ>eAW2L#w)N#87|)v?EoEt;bnihaV!7UF(tE!GMM7ugDiG9P)-zlqsuexW|7(Bo{d8NmE|OC z0u-B_>}@;xz-Kp_%(nKCRR?CI;H^|EFft&&9pcwn4-T8q$i>f8k=e^xOO~lz3AjT3?!5z`X^ma4=Z^^oJHkR z8xQfTP%UD6y3;{cF;!;4uyK(52S`S}QJc(bYT&k*{_>&=vFPHxCJHbjlb3nJ#aWsz z47w#tTGzCPpR&JtHgn)aHw@d$6EFvdePK(IwCot3lnNBst2tyKb9oUB9L^dl&3O9( zr{m?q;Q0EqXUCa@5oyRTfhrTOx{Ai$YX@G;`XMvaD8e&}I@mp9$n`ZJB8=gSgT;vg zPfYtKMXJq(NXjGUcv7uL>&h{R*nQNW?H5O&5bu>lK0g*bM$7&O;{DNZfI{cKO4`Q{ z5-K*Lmqu1CsYgra;Mbn-Px+R);PC=InCNnReGs3ltB(z=m>YsVCez8o3)K5~%_(D> z;TYqG2)G8~asFgx|FJu`%eYrV8g0-a17p|pl7KdXo{scgXK7fYwAJZfbL#y4w@A#d zPijaa)xK_|Y@TfIZnHImO1)i_Q7X-t2Fv?41YOd%YGEpxzoDT`o?tWKqLq8(pQ40lR0D3v<1t(X><$R1-K0QDp{6f3X>} z5#iU)5+0C-s_6V1(rP6P2Q|lBWS21q!4?W*{P zTlYH%>*!gujjq9}D<7HY_7G6*BKF^_@~>>``$!6Tu9%tDwzEAnSve#|w{(Vi6iVPD z{Ath-igpy6)9G2(j({jeX$C@IKlk>&vI0&^f0+Cro+qd>5)6<5Z-f}Exb{)Zcy%h+ z3<^n$qa$?6;uzbHN8fXjNipFGJ4Hdikw8#=OV;uOa3Nzr`?UX_f>a@DO z*2#i2;S$*5%eB?A1)BDgkOkKc=rT^K>V&}h7|lTsF)n`LOMrCR)L?u_mW^qIHfC^S zR2v`0^Cc)9oB@){3>+;)ECxitKhvZ$06PMP%HpOI{AGME^CX@Mq}S()a)rCN`K`0jNwrrj?EVSnEK)7JV;|-s zQTlR+;*EajFPUlqNOqY)Jb1A(MM^=l_QdirYSq5XA-zr3r?CwU zG5GU)#tgtfGYs=nf{4b=5Aht02CiQvPw_8b*GC zheXm>aObsQstfy$w72G(Un^q~vs&x1AxB~JEGxT`4m6COm))a z7_uRs#LkS=B|ex#dyV-S2%6;7F8n%`;KYVhTmvxIFxHu%u91@MK%tZ`(4AA8+icb4 zbceagy;*HJ{-er4ek^oGptWNk->L+{XD5kI&T5~FbkIWns&2AWw%mgYPYB^o9w|eL zQ*s;_FtRbis5YoO;CeGiJ9SU|IABKhioC-p9yfcpO?37lRG0;iAKK0$t-BVA!>TP?ot1^o(EFl1W^2ESDCIwSPB zN>Q(p2lY;Lcmp#3VWN8<$={Y+3Sy+1(9u(k#15{XmvP`toz}$Q2yauMf$7sT^nIeY zJZ87c5gap>5{EdBM9wnM9v%U&lslQ1!*wm|yTU_G6G%$i`H^OUoMOZt{%y$(DYf@` zLR1MHfm+f>J|-w3`!J7aV6ni?9MdfgFoS;1dAESTt8j9vhu=dO5sU{UmWy!YElHtI zSI$XSt1|WR-GC!P76vEEQEn#!L7e_o)VJ zab+Bed*8*ry5(H2Kcnuh{2Z^4#ty29w!FF+9lF(8&ND~@Hep^Za)Y~ph?rM*7igjT zZ&S=Qo+DyZ31X7O94aoHDPY#*8GX>^uqzzn+bG!Jws@D6hO=ifK0GH6OP(B$eAOFY z&uU&&T9ZbDLLB$(zuEE_yNwi3KHD2+teJ^LY)s5kyfD;&dyw_D9VaFkXDq3RPo*MRchNT<<^{9Tge z)|K`$;y`9(t5 z^FE6Fj+h&UsypI=(40PS6XYnkCrh~Sj$N131E@8{UJKuJFD#mxdnhM&a$^4*$&rI= z^ZEgjw2}QUNd4N8h-2-z1Ofao?l3&2Liem=K-KAb@DYHe`8&IaLbH>S{v+{wS@#WA z-!{QAF3Ys< zIK2NTw>lAo1qpeYhNyf4QOX@~BY=>d=?;dMVFIE9b^&?Bry?z84VJ=k&|(W)hG#Y*$xCH|t* zSoyOkz@S3iN?4)Voz7=sQ!j7%z>;F?2q^@kWzPrLYpfG=f6fWXwVobCvg@}a?j?fd zGbXF1M!T7$kvfY|A;>i~^z*GV%pd-k#7makMfIZG_J` z|0VA#t1Ly+Ne#PJBV2;ZF;Wi*xl@v8IEGrxqWc}txdb{#3gp`ZHl5xcEx9@M+ zY2pu|J_{1zz_6dM7V`nDiFQxR1y3CHMrEIq(WO=X{7~-JBBh-6yU-$zy>b3%gu)4V zrc@Qp{!q$8*yh50HBw_UE(a#T96OP5t)wJndbfLEl_?AQ-3`H93U6+#xCAbv_B4A4 z(bGwWzLnOFMu0e$qXRXDFle@cTWq<3mbJkO3ou5B_&Ga|?r`BC?QRrYRWPxLqN}fVh`1NXy{n!d1GQr)H zB0F}`7uiX~-P18D^wPhTa85j3km6~;pU;$#3G|6<=GO^f>v3z)OGO^?8=qr3s<`9d z$F;7#_MwxNd@w&?RE}l*^bkpDF0|Ce;1Ww1+0CPn1@Q`97$E)pTkuJ5Hi1YHidXgyLxYDJAP+-ghBxZ-9iGALzYa>r z_P$*rl<%tyHs_8@hLuEQ2CKK%*^{LK$l_+1BF~|S6}gJoULD8XGM$Sr>K9`6e3dXtwMa%S9h1U+!#2;XqJra6u)=dWmZ??0HB=|u3BPIsXSf%52|A-u(m_Ch(r$qm)KXQC&~FpdKsc1|q$T)-?4 zgP&zH!r6p-R_0?#mEdbhNDnK)co#<0KPCx$F%gYb@JJMUik{CqMm_vBKI^?Y_(W%} z8Bg#X4LHj7Oy@|iEz=5@AwESAIijA@1l!$&RKYU+JpuK!t@ThjBUz2;K`zyOJ z_lz0)oY)F~F8(kuox3>U9J65Xi*OvILCu46Kj8*H>!&GYtyuyu+}guj9yZGavW3x> z?@fLn>o@L7%B_c7Emh!0>Mm|uBaeecmMh=^iA)zr$r}-73|jjP&@>9;L}FOF5(m;l zm{*}xb5PKSHnv;mYjH@l%hh6_Vg{Lv;I$48*o6-jEI6i6_DChmY@?iI3_a%#KBF59 zJhNmNuuv4nZVpxtwaQtv#JwfhDv;w&Ge26~uID7|a-OL~p=Ce}g;0&&q2xZ>72Hx@ z1ibN?9%od|jju6L-ZZo=%;F>*>?Q{QY2gzQU9ebjxqu2AialqNRaz$-b&vPIVB7KTKn3ze~AySi-{8A z{@eM5teyFLIbXEFpACVrsK6gaCh8lPn6L5=xQAG2=bVCM83fQ7SUfT4a{52wTme0k$(h%!RSnHF#U}*m}TbxIpt#%-Y+!lcPapF3aHw6_)ic3e>EhG1zF8DP2N2>UgadSLBLIAQM_as~6ZOU#^TI?OCC zOcnh=YC=0@Nug(JsnkSeLCKrWXe?ZDsPOh;GJ8^K9`-XspPRLXwFSWl*PVuur(xpj z2A7(j_+7?u4c7Ubm=VFE=b2poWj?-MA{-dc2U{Mco?(!#BOb@od}K{VB6jJM>0QNA zyZ}Y>+n=EM!Hx7&7wH36vjFC^Y(`KaG9=ThK8}t%fD#3MJRik(@(Q}YK3eK1f4zsa z+Vt*Em9OGSDI99D*$oHey#sazS;zPXVd8_9HkpP}V(Wm#tN7-2d?H?!lH3@QplQY# z^bgJvZ2cgskzcR?9f{N3cxRJ4-g*Z~4!5Z8nD?vFy^=%JhpRCJQO=M9^9~hNvdcHF zGTfb+v&mVyKMgtVoW)88&X7c%2{u9$rz^%52y(W3UEceI#>U;#C43uG-GR-RIZU*u zAL5t5P?HV1uM~gbMhT9?W7!4$Eqf@Ds82b+JzyVT^BXmn89(3ReC(q9CNX|9946*I z51e;K(@28(i`F0{?hfh$?^fj3)4^A=`Wl8^efeT-ru1KzzGz&Vv@__eO~qaK@1w8tMAYBjzxnPBk6IbB%|k8O zkS2pN8ow!hQaJwP`rEPNyB)$(xj6lSEWUmJsvm_7<2-ZXcX_+x`a2cT~XeQtnyxfH+yU}0bGx3|&ihqU2L zfT8ef^QMt?j+VyMS@*88JazKbT4%P*fg$GPDI9+ByI^caQ=|Z11U9?>lFk#iJZX|9 zc||Kt_cmQt+A+{;Gs2l4ak&8U83Pjcp0o9WZyY>IH742l*P|0(E)?U{BM5)o3 z|Km$Iv1j>RG_X*Pw_*XcZxNw`iGiNTVawB=zJ^(&X-cR0*KfcC9iosMH zO(;w4to86NHTk9oz z41ZU%uoB(`@MQP1tt15XIiS7(a59YLnN;M@HT(@9=64=1 z$XCAq9guVE?Di4xklFzJwBO0>;T<~A8X`C5M}WWgHs%Lyn0uZwDairpsa}Z~~Kac0n_muEl)Z?%5cpU_}A*rwB6&bD-2k zNKgUkk7@g5nrYs!(f%+)y*!=a>Hg-NRA5#cqJ#9onZdfkm9}4j(6VBz_`n92>ry7- z;G$098Az7Z$~$~h)9UrKeYN9UVB*!ItyK@Ld_I@Qyd%+%w;MCqV`qrxSi~6yOKb(0 zW`ekv-ZJ_!U(#hl>g9nOq&^|JAmIf$WD+2pWX?bijF;kS90O{Zi)5c9 zc^4(naib!>tf3%%E{rdW5$GW}Ns*mNi=%^w7qqvHFw%rL;fv$|6x_5MHoLRapVvWY8juE!~^(&a}D)}>5 zz!g^$_%=~n=a1kBgv`ZmanQn8TTxw|Jta4M?kdm+0F1bl=yfpR^~}yvLI__ zYq$fXi~(X7+;LlSR5{kRUE|rdOngEF%s>W|nF+O;pB3pj&OMFcFGgT4I2nU0zhF$2 zBgwDdFpIr0>R5oQaSi;gkL*tG=Pu2seO4HnyXb+LERYg3T522yL&Nx8UYRyY5l#`z zIm#2z(W_+=u@B6~(H9pX4>@66VYv}=gSg$TOi;M8rGs@G65YRhX8RLqsfG_gj$t@Y zzhd~^D^nEg7Eqnc1$A?S>K}(+!E}75%BoneA*};>3o=OZ!yn;qFdU-;%-zZ!AY?CS zQXL6_ijRJk-1QBHiC64cm-K>!n$F{YDt>gSI?{KUDRDKZZ)ySYqA;)NPlCp#ksp7X zL6*+;h`DUzc(PofoY+sa$faI_YFq~=oO?dNOE(Y42{P{ocjQ3$A>RlINp@3Yjca{S z3l5!1^wK_OBrVf`3hc=Qe|?M&)I1PxQC)-{!1q1=6j;%6415w(pjyjXvgtr_KZOqy z7FedgQWj1+i7{k+-`kDgCcGaI*Q*7khxo@B8ZvgJf&PMIcN#@p@q;k?W)@JjA4cy$ zU<_mr(-lDwoIsxpao~^$FV0q`ER474gP`>5Ikxt)@*$f@vFO3LqT>-Sl^{)O0Zrrl z%_efBqGJfRmc+ubzrP{cHDaZ-dGxz^P$5)OG2FJ~9auG-09yf&1>b7v zi(sxBw4o$*%*=(Ot=H+-Y!t+EP$c6_t6C%h?Vte4kwuFhCf! z80B#Ic78qwFN~TNNlDdPz8HP1Pz~!jp2)zZlRpCwMf1Lbz>f5dk??$>iPUB*DB)b`LM1Dth7VGOgCpIgL0`_OOWgkqt z{`fO?Pg^1-MSj2sUZg4;pL_r{ItH+gNZ11;wPZFvxaGan1PpLIB0)R-ASYNfI(y-e z3e)VrHL+b6g0I;73s^FuZ31hVx!uPBp_4?-6py^&c{9r`RhLb1Husq;6kWy(7S$sIQ%7&T}J`G<{-zXj3(G-5s}NK_0Ed4f|}q>kgLpm;u`EJVNy*7-8TPABABn1?SX`dZ8 z0JcLj$ivPXPhE^Og=sT61vMgFjz^#)&RuX5H1n@onfrk@j?WxUpCunip%RPR(7Gdn zPJ|^5G$ufZp*X1f#$X6q+7vIv;2j-{KpAp*ca^bHoWu7E8~I<>-yzRK)#~COb0Lw> zS&TrHgYbeVrDv1kg}Ph{N0G3P;tea9=&S;EmA5*Wew*7^zt4VDotR#s&OExy1J2b_ z>&$)EM#dH_tl>)t~=JM#1 ze3vxFrwo=RSs`4dnSvdE6Oi6g*gKFSYyX$&)KizQtXcLgGDeveIUR)FoOWfmUgIh> z`HRMG6F!Xe$V8rF+(2zw=2yx_(S%MPkl)%wH*Xu^^bvChvh>OHH8h|+QRbQ#!Im%2 zd16C=qQ!u%Xypl2&kk?3VWlSxj5f?p`C^%)S21ww5)W=}2X2YoP5!3bYXnsO+67TJ zvx;hH;M*O-U^F@-;3>egXu9raZ!I{b%J*m~e>=OzTU0(ePsMe!!-d@6UAZ-BKORbhMUc8i|c1lnw3cjt+xr$*TQs# z>I#Iy`;INhMYC@k+~!#j)9u4eTkEq|>&+$2gVmgLQHz%h;~b4^8g8xD&91f{o=LmX(pp|x zc^9lz7pZkCC4?uSS~1*e)mFuFO!(sihvY--&Dy1pEii-Z`Gun-m5i9`JnHC`jaZj6 zQ<$xt_4v(FQ_&LzF=05BhX?OSNq&Jt z8P9p)Y_v!_8fD|>N;oKlONo3W1TsDrW7=*#lIL+TiV>t+Brmqb(P1iYyN;lI;u{)B z1uE8Chr917)N&va5%oXd4wqmI66ICDe#9fw=)Ud{1-7Ns{^{u|_Y8Mu(XU-F1FO=c ztef`_MZ9hh5Hc1*;g#zGrYuS!*3@?vwH7p$Rtu?OgaJ=2$0ZIB$E)5X@7H6&=Q*?;E5*>y5*H&2_0$t3od5d?98Pw5T8v8y!l0{lh>QPsa^f|JRJZ6o+t32~L5C+%M;s^+t!V|WkKk>wwBa{TDDFM4PX_> zngY6p)dD{stNiKu^&a}t)QDGNEOTgY);Xu&*;yz??ce~#HhEbc(k3*c3oPts(4|qxu$Yhb+w_~*5nn64D*ccaC5ac?U~lNe}%7>h8WBO5uW<=2#X4}S8!>R zbznF1Q!BK+*{>~O)jU+I%2CF6L%+Yd9GTGlb7`D|_$jbP>-kgh!6>`Xs6Z)^Xg`U!FHy9pV z?XMoB^W*M{)#$jJmsTpYqkc+*p-31>T3?h^Rom$*TXFFXt)nL$QgR)r&|z3$``f(a z^2ufYtfsCVH^~h8f!w!omnu%>|IkCwI6LvOI9MkzSQ~hE3aK$I@CiU%-um98>WF># z!ckGk!qtW2i^D31=e#Ao^M9y%T3gcn(Q@0^ROvsLHF?+lGLHN{HED4oa+4}qLcf&; zTs-;fd(+)j?AP|^=k}@p&(Y#Ko!UBH_RiyZ~*XI3YvM~j^xV##xA{TYc1p%4@TxIIZ`ayZ*kz;q01ed zsJa?a_x-+v;hE{h|G!t~K>oFTM58*CdH(ggME;A8mjSbl0W+hmmHU4?ME<%a)$l*p zeGI z#WhC=|3mNSKh*I1(m!1AU%lSuU%ejn|I#)yaQi>n_5TxVxJlhx6-@&-R|*88EF~O0 zDkvqqRuY#Q)tD<&P!{hXn^Hbs%t?`^gh6~xCgpI(siuoqlCVfAy$kK4rd7;0qt|8Uy%wTPyO>2g;le z21zW3PBtY92cM?gDNq+06}7o^aH^W(>m5F&(Mnbwr;Og)5+%o57laeJ&29W2b@9d z+({j?JByevfk-!R&MRPu`ZC^<#mN^$nIpf8zIceJ%_I*dYTDd6Mw7n!UT6YrOwuv{ zlFW%uvWbcZ940cYkPOoT*K+p@78n#V5!)(H*pIIlgKcR1>Z+3#=}6EK;GRYrq>6Ci+ z20h2)Ie7Fu?}z`s954McGizqnTK8IO=Khtt!DQi|;SaRYGeh5d`fS~c?v9Y5yqK%Z zvpS#lWB!=nxXcZNhUO%&_<(F_mF)RG`Ug7Zb_ja-yGDmB`(`Dlqnj=B$f5+u%cO6d zHn}rs1{D%a*4z%>k*#)a-W4~fJ+d&t)8zAnFsG`k(Ard=@2_{LE9$i|QFn1xlg~Es z-2$dq*XVJ~Q41e)F_}8C9u}L~c4rX7Pk3@hhbD)#kgcEM+*yAy8E?FwcP!q~T@V)w zk6>ksh*+QyI1_z2>lJ_vkg-{+X8ICjP+PTFUf)LOq#TGCqZsfgsY-r$-*8(EQJ$r4@9?cw z&ej%;I!6?adevILXpj5iBPAtMS0mA){L|H(h)TT0J(U5^8t(|R#}#E0E1d(Bp53)N zCI*HDAx_Yr^4X8vFI{DAXr{Rp3Tfp|<4meA`jT|IF7;spU2Rz>b+(&EY_NNfCY(64 z^EVUPAbM2QUvJSoy`Z?fCD36~m4n#9-#G6R^|muMsV2&uwt8}`vMx(!B&;NZ z{@w26DCqHr1M&pgDr2=Enx!JySuzuw)4>=&mI*zoGh$%t&MPp#NT=pRhdcm za=oSSB>~Cmpig~RZRz_lyIf^jWyGQbI8R^Tw}BOO-bU=;X?SfRB(Q|sUqTJvvAV~g zOfLnEw;n_IGIv=2rH{R2=pZp}=i7wv!+v&;mN<0j;PWj4dilubx&n1|JaS2$i@JD` zBf`>=dyH&Ilo6rh1x5(UO{d^RSuv$R7GQKA=r3W)F@2DVN(V4O?>{(ezMg64i%^#I z88v;Af8KO5)NSjuoV0?oqwSOn;H9fOfKORR=Oe9q)~F7C#7qyh#{h12lwg`F;&jV; zC?h_ydsjdoDR&sXH4oQn4iR#Ck|ca~8r8|6;dqzm7|7yP;Pv^4vNoq@(X2@M^6Ei7 z(D4F)pqJlQ|M>bB_7A?;-rC4kkH*wO$K(nYzyP0pgW3jMgus>`d zaoyjkbBut_UGJ(h&|7U#`=PTdN}|ng7$YfFB}^kBb}m6dIx1fI?@&Vl6$S;nwmeD| z2CmO@V2{Ux7|??U*ZK+uv&*wU_W~*%uZo9)lITthH{dSH*$x`1VqDAZmFxvlelI1n zv|SgnUE+8EztX$3U1GysXuydpz(G>>WD?VlF7a`f6o}T}tw0Z;7~Y5Q9fAQ#nXL>* z9do0UCVxmN0hAEb{wFC%o?lCea3v*34n``mIplrm-N^Jg2LL;mK;1(|9Ev(r5qoze zYWHG1PNMP*DgYg7fiyW5#Rq6DQ^Hrt|+Veh`9*p`3|Ix{zcn6UPLavMd8On38|{4hKq7d zWuSlhX&U0Ybp;)riFKycxEqfSh|By>1^M5P4&7fA#LdHju8|QfAUu$pK)cvmxW-*w zSRY*W3|}^xw7}UM;Ypk@K@Op=47^wKEBI_m024@6TE?*XkPrCFFxSZ5905+y!_u+P z9zB+2^t|GeNHg{MD1=Y^osgizBe>49b)$X&56?Fte|CC)*$PbOBi#Wi#UeJz4vZuD z$p<8~G}!EOz=9})P5K-&pxR4-mKvJm7+jt#5v{Hq{lNY+P9sM7H}5ka5!_)<7AezS<6PkUVN z;|-to_$@p@Kv~1VW=uAq){`a;V zw2^)7cojF2ivYjT@|9fVXZ!d|lrp-s{}(A`f{}x}sk=awG6VbnnNo(Rx!j)XyTb!A z0R=L8r8h1skVEh5vi{5h{K~QpO#A;|^&fPMT0sBtY5?dzd{&&_yyFQ3fNq8gPNmPu|K#)dHRYxN0y6CR{`y%0Z2l9L@#}5Me{=_`5cgT5&xqS2mVMqi8^>bi+ zd~ycwQMd^bXJfZ6=eFWxV=w5EkYYc^6%&Z{7+utaW3EUetEXZlW+vvMrDtIVYmU54 zRFD=j3>^Q1B+NTLT1Hy7Tqa^U+Pa4rhL*fi#>&Q0#_HAuEPW3CJ0DIQi1QtYlhHnB zXUCJL`@)#*gx0^3 zQJZwe{Rp+y^VqauPv~r;Dh;|cu|r+$X=mkgW}d|uzKGYJZwE#WZB@ z$L!yoe!;fgLERnvO!ZERcT|@ed3$4{LG|%$TxCAZUWAG@D$5E7H~N8;*?Dl9iG`N}#}Ug}S?+@R zMQ!%RcugWyG=yRc*W}vcH!{ORMLE)&qDe8M^#WEQsUG`?M?&Kx(TWZQ} zBUL!xHOVP38#t06v^mjyt9JY;X&`T8B2zZ{<4*B&@+l4McZ~DJA(XkFipZr)S}oQ_ zmzX8PxpS~m-&}H!YzRc|9+~PMev)fFwrH!^7d(izAx&65k4m27*buVi|Cq^ z)hy_x_M;W)5|un3mQRRHj@3xXJv%+xx+dOgY-Yyk0{_!6{b*50eo-Cie$l4v+x^w1 zQ2%oO-&+-Dpi%s(RRIlSYVh$qA82FV0vrGD4r8rnt7mR&WMNKis;6b6XG2Z@=$nlJ z4GwC*Vq>Zi0~l;D0x9N2r30-|E26Wpu?&`$^QjEGg~h2Ps4sS2(lZiQ5iwVg)sZo? z5t6CpZ-uAmuP)EZ;K{}rt`Y#kqwiR(sj72jl2tsUX9TJ}XB}%rF%vT_8#7lC6AO98 zQ*SF_Az^)UioV@R2syz-BH{WP=GUz!*#aN(xrj2cGzBj&PEOt4FW?{n`W~k)BYo$w zL7SasUT@=*NtE($bW3qM|@1KE|fb z7f29;wvba(kRr&G12q8!wo?#J=r-27kr<*3dwX+?GA_&zSlIedP{+?tE1^#0atuhF z*cBcsEF804T)Y5zZ5Xol)A$yc@4WW$4~LHSm*upz)-ty-wJ^9o-Jbt;>_7ueJeZFq z11_c_us#0$fxeAMqhn%u<t`kh0D93pecv^ zWsV)gce{NAoE+52D;FT89&o3@z_|YVcv_C7`xJKyL@aV@4qx$8k0`ZgH^FMbB zw0J+9#y=B+bHD`=eb?Kd4P1;AU}OBvQ8eiFY4ml?u2w-2I1s4)FxdA&rq)_ad_tu3 zMtp39@@%4VL_2U%UeS!526@lNi|hrzcBQ7Jl+-t5P3wS)3WL<9A%ZF4Z2%KtWOdPO z0HYITRJaGO8j&;`6gmo`q6Ejl38t8a{ce9j;>5?Cq;UX6;rY+vJpWg5mag-ke-S5M zy79+KYw51;S8~V3H{N@SS3c$%@_{CLnJuY*3e4XbApgg8H~;Cn565(x57A-zB#Vjg zi_%89r(;piNso(XDiJ|ZVyB5bz*2^nEh!UoW+)|jBcq|L-<=>w=x(6d>%pQ4E(zE{ zhnuu*%19_z)5NS1UKm~GUfxT6zdTAM z+*HZBPmiC7Qg@LdK<=9`H7(u+Eau$JN5*(jOO8cK@}Wb>8U1Ldxms^rAI}NLw*TSB zVqb40G=vK%jr?bvv8yy6sDna3y;f83M)i0OgA-tt;WO$smyQ!ZPmv`@UVf*Rf`gC# z6Kr-oW;o$+I}&0XoM&FZUA1jlnuI2YTzi)Qvz{*jxNVw8P~MSYv4Hih+p8a0vvt9m zA@f&sGw*dPy=iPDiy_G`Z)$-Pe|~~9o-eyShnd?Mtk0;2D1rnK>x!bK>dYduj)?&j z4`-pqp~s+NuuNwTa!pn{=IH88PzG})L|5Y<;@#O_2|_hT?fLksq5Bd3WODU=r1>=D z+14c5mm|9vG4;?D$(%MCu#TT%FO-wH44Ux!Cg$V{z*^)a;h!W++h%T*6~!?Z^;nrS=xA)sbgy)S3aI9w_JgXwi0C#So;TfDk2+UYC4@}&qovsIg54kqQ6t;;TWH@p*F3x z96bpv1{3n@E`c&ELIOJ>a9MvZh1s`PNR8Oxq-qJ>XXU_EpE4WD=8Dzl;(a%4V&^GY z0JTo^ix8|hv&H*%wEXa89Eop~$@0tw8D5uvimfWZm4;+0EGQ_b=z5=EBpwTkXWP{; z!1K;A#+fYg3k~V;9sKp|h)rY|@SWPYC+U+1h;^s2sm~G_G`MGKBGiip zh>uRzyCu1EoH_=2Rq8Lkv%M?8qZXYmneh)GOPaRv~yN#*FER|SwDmt{x>rkznqp2PAn_If1hf-#Y2 zBN?Nc;K|Cs1&BkRv;t0K01n(?;s9||bO5hOnm9QYpiwg~4YDAS5t!VJOyB)PBu)%y zbOJPfrg9u32e6b$dLtZW#zRFf1Kc~_Ugvoi74A(YB}c$xoq=Q7f-9ji`wzlUu%c$6 zuRZQj0r0w(6L}iX4S4m5seP?xRlZ(ry3V&;y9y9y>5;pq%?4c4+Yn%2kABld+vpjX z0dc&I2IC)CR3-dEtG049?6N2Qqr*8XJ4EiyKp)Pwui@J8OE)R<}_vq6{?n!40ElI}20 z9%o%O=p-1T@slk9#{)HUE#*VUEdvo}$6Zm!z#wmEE}~@;f<%q~Q$$8f&0Nyi{_(&- z7iI}y%7cn=q;$KX%d-Btj)Gbdn!$8|_3UI<&2h$1L`zrAP}K3O9$RBkMd8EuwIh(N9Svdn z^xv@*^B-911_S%O0{jJh+<>LjzG5jfnD4Mu&sQu(1H@8yM)0Of2|-wy#QeFIHOqJU z3=&5k|AWJS$5J41uCdh5#Q9Y$HK6wmOLgX(=l?L#=x6CuNB+id90`hJ(~N=4P-8**fmhS;Nobpt z^Vv-E+vZfsQ2CHPK3K+pCa`n-U`wmpvK2-u#?37}79y?w!h37CQs2KQ?JpOb;4pyY zMHSaZ-YN+L=W9%z5j`lxVVJn*?Y-0Q6uiE+&5I^EFHJ8ogmxRiv47u2+Pj>>GbB^N zSBUJrQyNp`{oPFbcrADgEwtg1Owp7MJ(E2L)RCW_NjWy|8Xp)U`r5l~D_Mg?fiX25c@T7b?S?xLW-{Y2;Fx zf=w$!a@o1^z{OM1Y*6L(o7JZH4I$bhc;6P-*U|d5_@kdMZ;h(hi@w`Kbx&RyZm50@ z6B9%^Q&{wg(Q}HLlO>K1yl}XyO`T>@<0IEOPi8n0m+K|+pEwGH*Euv5WbOhDFb42@ zu)mL^^uOUKIv|eHefCr6o-Ef(`9un{?x^R9g{8ytpH5&+nA5Av7@j``e#0q zU@NA@m1ai;bMu@aM!oXmi^0~_$`%$u(7Q*!Rcll8*r4WAT|#`>#h~tVYAeNQtE)kr zJq1@ifquDbv#0SSzC_mXgDNgPj!ap8E2fT^iLj}j0Bho8CFk*4mH8)f!9!VtO_QFw zd$?YYU%ll0B`BTu=2;s z8-|sbYq~FDuu0G@%y+)HKZ+M}YHTeZgnfMYIS9N)z9Fss@xzb;Mu!#;e-F&W$sR>@ z8`=O`)VptAkDfT6Hh*wh4`X~*_dNN{UiM1a-tg+}1^dvzULkOytR5vqLD$DDv*Uin zO$NAWHpREh=k&pQJCU~&#QZU1*UG=RW9*6($Q!JWSt%B#J;bSvlDypMiM*e^ z2js98(lyIgMQbATKq2yYN8mxv3+IY(*rrb4H}+M%slm>T+Y|p>cBWF|<>eOHq&YPV?!)rR7Ad^$2hysP zPfOm{ir#DGF8N3kIYXn=gI+0ugA`Gy=|A_zWS;d)%v`12@~w5XQ&LIpzaGlIb9lEG zAj1bdnE&&kyax9_i{sb6I{TT3)H^Y8G6~8X5sFt)eAZv#0;S}VM}82CrY`yn4%YlU zkj&!N!mZe_sEC;G=)u0B!4c-Zfsf$E*s@I0m<*Uu5K=<-h>6qu_zm*{{mbal;$SE! z5Fi9vFc1-}8NQ~yL<2DfW#(nj8Eq)ABG3cl3NQzD_il?Y+S$m61~5dTdSux}?uqV+ zNCyV+V$)gDSu$?ZZ8L1sZ!^Myo$w(wA>BusLqbQ2K$1opKw>~DJ-Ecfy|^@B9ARYo z$OvYv4X*tN<{69x42@sZI5-U542(9gQ~{%(hAtn64kHCa@5dmI$O%~t4mCuN>y1#I z#mEEx;7J%GaRpV~LvHR|T%{;t7J5chB{0-PEb|AGBs~LS22V9LV^P5o5Q&$Uv6h#! zl&jwPwBY2D0Kd9v3+3h0Vz88*C9gU19sxyb!&>6r-o1-CUPK5KOCq;Sh>TrO!m^m> zc-yd5_d9O~vP9&v0=|^MO-};0--p@yG$y(~-D&aADiKI}Dd34gnAwKdE70pn>$)gwhX&a@Z5;aNy8as4YOorU+cb>j$?GP!EZYeC;H<|!4Xi5_FaAR)?;v;1z(R+YBZ-nI3 zmK~0NAs^|V|2;V(!O9xE8Y^ol2L_6 zCd-c+=pcKU1jC$F^Iopci-bAbsM}CJhH|BVja_Xm*YuTE2f|t1%<4wf#bx(h{+B)> zAGw0{XGaZ(r-#{Jj_2cz&?deQuF9Htsqol>kITSD@7RO(t<|c8j=Eq9VLeCwCh|+0 zhV)BeyQl?)ym$+a^E1gBE{$`Tzba#Lv^P{gF#MVN&-?OIw{V62zQJwmu+(@3>5UkT zs6@p%5d8axAd??o@-oCc79BsVQ*vqJZu9P1#@a6uK5VKg&~GE#&G~Q-&UhCQ>{vhLF+mt&N@N`)HrxU{bhvwdZQ|&^zMVmEoTed#c zoeYu~F=q$@3M!OaKM2^u(FojOfP=O|uJBBwHiVi&K#`DeK{!H?K!1XB_X+z7DH)0j zbg(cJtHVU&6lcA`xt;6ePDtYGXC`)fFUv+yLLRaE!ny#CAPGy1F>oOBQ40(#Mp8}A zMF=0Dq(MSGOar4JiXdVlf?WVdq;rSR1mka@(}WaA%d*>{Tq4T{WD6!cU>g>98TAx5 zDao_7Ieg4x8w00Sqskd{rOfiPAF8w{ZOPc4C(O&p`cp{xeLbpSUHy+zQ+GRPr z%X#W(<*+<|`%fJOoieY-*%*EQSw}%TaIK>^?7(m8=z!io>L@rsSxqtS7KJuccCdRK zWUe4?cW+M*)G!^aNFo?L&ZFR8G+IAf>FtXxlI&9giFR@YDyPu4hVjB=?>O`;?{vh z015{w-WQ*HOq2X@e1|3`USI){Mj7zy%>Sc`+8X_I9h0RWTMPm)4|gi2M!f*lPo5&@ z31_>>s`LbA2?CxVJ|#mBhbyzQqWO#!a`(hN53{7Wsimo_3F5(HZt(o*l?FcwK^HKx z$x#R9R;Ile!eX}Pq2V}8knKTgR>Cb zRM8ReGynLAT%m+T{6+Ss8?C4r}U-FbkM6i~GYnsyS zi|L_xW~V*R@QK^Qg`eH`&CJX#ecA(;raLeogvLs2WA3WOvHu8OzlaO5t?kYO&Jk2} zb0%SU&*uj=4gJ9sW7$ouNezB}%k>cTttEs-e7E!!yNz}#TQ2z7+MulFl6Bawo0>L z9vRot9dyH>Vbq*v1N8?@60kE^5&}+K{(qmmtA>2x+@)(fFGe8rE*n& zYXrKvoWHrbU2<`WZqW)le-=$1=qqPklAB=Hb6rZlPDl;|t*Dd3!$L5iAut5iu735p z5*_WIH3f!0GA?|;G%u+Akm?89jMch67aegx-`K`46b?xyaLI%}%q<&4u?PN@zr;_Hqh z?Df?rFrhd;7t*&IN$P@9E{kKmLQ>BAGlL`N8oj8oD4uOL*TpL*)}|9=z+wUNRq=}C zs(2;peE)1ic#6raEa)?g8dC_fa|<5~9JFgIPJvvJZndyAV&?F>iam-GB0o~AbX-zn zH)Jvb#}c+4KILe>&e@j>w*qz^k#%SJomC3z0~!=@pF}^iZB(l=gj@dn>EzxUwslJF zJD%Mbm_2;zystZHvz#)MBB*gUo(i9A`vv~>U`TLk?p~f%c^F^ zlnAc^(00?-CUMq^Clv@UEBEqvP4c24s@OTKa+%YyTd0DIW3ZHBVmopu*V@FqM(f%f?j zXe^x2Xm%ol!==5|7qNg3@EDxz^O?$9BXM$ijxYQ5*zGC4%yz3ZrGNJPC|#=oy>WO6 zw^92zd+NluA>^DwGg%PQBvj-o>Eb;_&dC_ZhXPExyl+3Zje1bi!hh1(@#!d?>dUk3 z`DMbts=AMqbD=6wQ$TlFem~%0rZIo^)4J7_(_Z-&+Qw=CH3Ssa{tZ!syfq>uEF`HX zm?x1Z;M^6)=kgM-AwzNO74RdJ4J{2_4Xq90)hpMZaZHOpXD0nY?Z<5U*&KjE-2RXF z&-`oR=x4hAM~r?Y2rx8=yHS|ikWhyFMz>&KB#2rO78&Ru=vku7P_rF~jB&$#diFUgZv4U=Fi)l*g_t1VPB`7{ESA3wo1eEa3 zf2uhCktw^XIDUl*HzXyZ!^KT7cyZh8m&T?ZiAoEH)e#YhL1LRWFLghpR4x{w1;>(h zjL=I8`aH8KF&eAQZ_ar%lz}7Z0w#%YpMc{8d_N7gw>49PXKhn|SDLfRck$>f^#rwd zFSXmEM|!sK!Ae@3%`Ct{(-ZcbB;mD-!klYNL-Fm4+@9UQDD+ z3|D`gIOcBCAij{Grl{dz?Q*Oftf(59kAhxw!5nXyeaIRLQQGz>&#xxz#2str^(|%n zK~vdo<5m%;NR>BR_MhEurEXrnl|yMr!9i|TY!7uksZhbLmd91^FjQDBTquvOO3E>t z9_>#X)Pa3Q=7qrna}PIT!d3XWU)lotrhCHSme|Xzicb`YdKgUQK|TPB7|M#u0V`p5 znQ}HP0DQerRVdGw7rrv+*0W=)=FdVcm+4$ukE!RJNyF7uIo?5m#Y5l4TgwOsn%}U9 zutS7UoN1l(F4I>JPjaR5ylV<5t21gFLT%%A#B44|>qb58?yBqD{Wy4!r&$U5)6A4` zaOp9mk3`F2SeUSc^ZQT?Gm}@Xxpgy=CZyb0C!EIz)<>v+8UxVj@@m4t7z6@P17IHZ z_l*JQzRLAgts9P1ww#$MhTfe^&d=;)!LNhh%5?g6F{8C6Ce?tcVaS+m{CQ1rcB1%) z2ZpGXqbys6?bTFu?*4>lOz4cwu*7~+^-qJO>UX-m(qR1iixP^_&~>QsNcOV1?5X=o zsL@t@4-0FTFP%;=FYjKkjZAP$_vAq{bHy3&;nvK-Mh*gE*Rj_G0qZjK=yJ*XOvxIOP&cz%cR! zFJ7>3OW0G$a@7usoCI;oGqPv_C4_+fG*(s;QK~!o;g)2#u-jHK;Xg)8ljrIk&K(BW zjsZgQS6^i;k|@e^hCd2yO=|QBKdhOL%R(RDhL+e{PVL>zP}xCfV@uC3Ds1HVK&k9L zouI7FEJm!6;Qlb6%YQ=B_euBzXv6d*?u@leA$#lcSY#S9o;~Gs(}MSg_oDmdq6cyK zrJsa|(0f$u5Lvk*#X9-Dk{IVRP9z}G%QN3Y9rv-~S5%r9^M8l|+e*fdy6(G9v{Yh$ zq&N=1;v@i1g5wil3cD#%9!3*YYNeufbGy|>@noX8)fSYh0>J>M^{oXG-OVX+ z)-O+++3j(x95dB)m$T_2*<`7Vd52X^KMZk|TTzE_JkmnjVmX`jo4|PxGV1X8eb-ct z1o&y6#OIooxf-My0of7G8YKm(EkpX3a*bcap+;0xXCvPQ&cL+G!7&Gd`ejW$Q23{Ao-xRHpWSQ?9BFR_n=7# zl2b92yTbyeczn4nDez=P^C`mK+F3rimdH#{z4tciWZBBLpnl2u0bL||x}}GP)5zZE z<9<$5iibwjZ0sX0l2ZGH_wgHEW4@pgw@;%M=R_74*sCnaMU+27HiA(on++*HQ!_e< z4$QV2S-q9rw2R?OSFT*?H~W<7kX*Y5(+76mA2UTvxyyOjPF^jww<$C5C7e7~t){(x-0g_Rq5Oc_d+XKOuB2w7!NV-L{wBsM-k^hxN2tCGQQox zjuSfY&z7lwW4Wd+7UK2rLkfG{qbXs1sZGYIyn`_t-G1e~MJ^B3cvvlz`L-vKv$MpH zS`*KDft-AH-NWir2ko!NAKo>2Q!sq5TmBp>UQnJC$KLq0PE}sp?CT)@;!kIUwk!xs zqyuv4*$0W9OJy!uoo$!WkUEHKP1ca%Tu`}ht>Ii!0km2}#+yr;qsdjT=?S$5xSkp9 z?ISa3RQ1Z=IsOhzf$ZnxLL;gm5QEqN+wTYQfAP-W;-(u+=}+=({}?xQL|(^Deh}A@ z$GTE(8`uxn!Ue~X=D!C{f0_Na!0BhR|HZ&ba_Juer_{+kk?#X1Q2HsI0y{4P=)9KU z{)_VhRn)JY*9`zLJK93*w%(n#ddWuP6Gj*+U`??=vloo0E@j< z!8U&T@n>$Q5i>8eczs%DE$lLMelaFSbd3Uk;D6KU86|@0!6VQ8>8o`Ezaa4HKkWZ9 zX+;eeY?b+5)`MiZdC!Cdzje<5bB;n>1h)X1 zqn9j{TUBE_yV0Q+2(%1=JfwFDM`+bFsvXT@dbycS2TyUPT}mHM+ii9d9;wQcnk1TzG+BT+38P~Bkj^KeKq9L_l`M*9EFWtD)bzQ@7XU%1*iwEap%Z&yuDW zTo)u8c9?ZvVxX^OLo&Bdr53e7vkYQvltT7(Q*Xv~AIMy2iJmoQxWz^V9vgu9lhJgeZg2m>Q5 zcbhQQai-@Yt9j5xbO&+Uu<61m80VTpvV4uNuyKA;`davqCDqG-B-bnO|7Jk_E^}`gY>|Hd=ob4EIiz9T(V2f<<~2E>!Wx{XhI{aC#|W9 z3Y{5Q*lX`pp*K6NYxa`n6)wjCdW)BLhWQiiT6RAmY zjF(BbyNkC*DWDt+mhs)^+hdgW|cz|^5}tH>HkQ*pfOlEoer zr2FwQFSuK2-E+s;&08lCFBL}TXO_>vBvJIx62X7QbkBRvh$OPC z;rPTYIcUE9d3VDrA)Ok`VB)qnhEMuvp`0GT9R;RaLM55Lb{T)MX)sD?RJ{HEyK*(i z`~hse2Oj|QMu`9E4#giep6~Ba++Yd+;(PuqSAPqKfyu8QOV-yl9^h?rhBIFX7&0C@ zZ{DUmNO^9zL{#{U=pUYoc_Hz$K*5}a4>j+Xp)mwO`*v#j?A%{9(0i|yK!*`^8#0?s zvEdmb9UR>Z*c^ZzS|pKY74NBWrKORef?tn;fglc+mgWr930O0(ARmgJ=YWZ(KL2WW z3%e~N0T(P{LK;*{vS#46K(CRmKpz}N(kgY^#~w2Ckw6S1S!8MmSgD-N%;!4BEvA*<3OCG+u!X=6bv! zvfXusLl)8J`LZ7a$;uZc1-(RF`9Tac@aqm-?%#zQ=n}%WW4x+c~MbN1vr6Jw(a;JJe4l8z# z?xX9&t5>a+$6m3u-vJBI(F8B8-L(NGEp-3WO^P3m=D(+#{>ak&+f544u?%9F(bQko z*3z5TospN74pWd(ke7m9L&KEC#I${eatjm8`og5O2myRx&L0|@E&hW)ar}wHPIjyR zYW@zne>__>g3-wda&~-mRX(vm{0CTG^y&{M*}ylF0XIJTz}rq{+MhCF*^o2EkkD9~;G< z+6kUez>sOW`&s8?#k9sQEH7g|2rOM7(O05(|k`&~S6AxM75pe&rG`|vhX)ZOp48gxxL z`TnwA6EHW`jPzeX80ebvb-Cb%Ys%g7;)WQ!m?`m7BmS=rQf^m`7Ant4gtz|yE3EdCw~BfATt`cN3ik7QN1T{(u3|_;gb7Yj6b<{Ez`Lj( z0nKtPlmKY77anMjXm304!T330Jr>mL@3bY$x6#3*M8P_Sm+HcO(t8Ul@Zgy(!6w~F zAC7J|RTMKyKPu+Z2CCJCW|BA>ryP;&`!bd>7PJMa6b6EsL1C^>4o)+l?73j~0-aLm z;y<6cEg3b{8VWchSJ}r$y6xohVYrz}%G>Pq7iOxLJ2P|Ycf603uZ|<_n#vRO&CxYQ zB!ct0Uv7f=I3>(aF#tco?(y!YH2>o#CD9!xHNcn=tv71BLM)ueJR7!7lNzc>PBXKu_!Ot?7}qX z17MA=;b)f0W8e!EmH1B}9Ac5KOB1SYtMST(;Q~H#hmWrtt;IDkviRw=UYd}8VA&h< zZH?yvR}7!id%I*EplqlwCU5=RE`okb&1E}8>&KNSzVbh z0hZJktgTP%E&I)TX|rwnn+AtcYSmj(s zhgGTL%{Tq92rF&8PR+O62$P3s3^5%2QRZM=e8OMFkvW`h^neMb0N&H5l{JraC5Y6> zj#%{Nd!D(Nlr&jAG+VuW`C2&q{jCzV7ZRvhLVGa+`p+m?-Wf!V3d9;ZsBE`k_Gk%&+OT^r@Wy=1#;mw}!t?ub3&XEUov@PatVlI!z29c93-Ni+cWlK)LYyLpEH3!ExmWqK-t=?L z*VHkW-Ky zbLJo9ZWUraumj563~c`;(fY0G@-tz7q3W`$8x}_dth%s#b3~qCVYDqoT|`YpJw%N} zABb9sx`~>JR4!B9XD?mDtYHLSnaYE%?!jOaQFjmteyzu7Hmr|h{vcr~{}#n_pmhJO z9HS4cEB*V0tv?+7&&x4DDRHmMF`S@s3^CN#a?D-$Kg%)s&xab2sJ#ZX(U?u!ji01# zJmWBKN!zHZ96E#bxGFV69YcH>{7y(vtrqN5I!GBPqz3SD`Cet^i{KZHDBv&Xt9!f;_R8R_raxW$Es z2a?yvHM7twHF7K7^LR{ILM2s1#Y{_F@7G38T%XngB>m{^Y)yovkdjS`Pi@mRe2+T2 z+~lQ*++7~reQezz;en{UqO^invW&A%47>naETMrD3)cL|DG@b6e`7!Pf7pxnS<`oUC;biLib;o`G6iw zCRa80 zYn#v{d~=}0%c1lHfEJq-8kh3K2$9R!`}sCD-!qlWsj1?3@SM;0Ha-+(z*@NY66|AO z)_WNymbft&OOIqQ5Ax`^Dgh2e~fRt(PvX5sO|mYR&(3&EJH9z z%)S1VoT+!)B^>dTr^=5gSWJZgIfiRz+eWIRqbhy-8Ok&<1wP8;_>?bWg*xU@_~a}Zvd#O&nag^%;=)jk2B3Tl06yHzh7enh z&eo(L=fR<9rxnCz0E<_PL1YFq)>Nmt?MKrCWm6_2O=qX69GwVZCr%@aBgWDdO&qif zI9O`rna<6r)XQ1Q!hE-gvq%!NvbdC+WzALv1ALDKTUlLA2YNyYydwRHCeHkO?s*W} z4pbkRTLf-sGUWeUp8ED&{RRN~BAi=tf(Q5g3!FJ7CM$H(3b_jHw5PU43tkyeZ;5$C zlz8}>`ueStWgH7i9$fOus!`Z&qCXPk0#mW);>CtR)A51Zxl7?_FX2`O;H)|Na^{qB zRFU%0JF~+i#K4u&*$Zi53s!}!9EulhIrWl2oD;A6y!vdlkI zE~fsrmZ-&?<#=Nr?WD`2z0q}no0pfYkrF^5KHcL|;#@1Q5jSj+*|3(u@&>1>g|zR% zWQdBIiD97KYxTh~7U`)5o3OfClep2(Y0WTGqz6WAL^g~>(1k?tmzWLn9AHEfys-K! z4ic8r%nrO4aR}C6A-kb_qVO(HPB`3h(edaiEVN>=lH0Om0)$)S9iP^U0=lR~-%u}D zuj`0;TV4=iRk*eET@2fY-kL-Kg+*)&$hOy$}X zPczDv6q8cAt`P6!Ep$RLO07m8IiihN0jVg`Sz9SXnD*VzMcE(ZXc6XX%GsPymL4m1085s!tXvCaWD6ynQ^q z^)9rAM{hO?yhaLdx^BL0%F&}fy#|fjX(!y#;qc3fqcbU0K}V=7$XZ;A?R zC&4eMU@os@C8NtPt;1sf)Yz1ljhFYDNkt4uWp@MJ(sl13=2)O3|Ks;Jrnnn=|DMwc z#3p;gAlFwfL4&;Q4L2 zQuGSR@^ZFbPAoQYjPBC%`4wRt3`5WUA_+F8?oxIeKQzh#PHs#T7<7QveW5StHVhbn zGP4lAB-o3w9#JjJ(M#`f3?5=ZiYHVHxs9GSyULq=wj3dyZuVAuXJ6GVXPnV$N>+)^_g@wVwlEyxu1ovI3!kdv{x z3Qql*Y>dduaMuZ@#*|a1K!nLkmAwrKiE+{`^6Kot=OG`Frl1%3Bv7ZbN}_4#<2QQE z9vYV%eNm5R7J6dFHLMzmm@>2{cBBpY?$FnN%c-}bd)t``Jw-`LK@QK@(!}-sR!<_z zm__nkNj#VwwdsNigZX7N|8a#=JY<5BF75IA=>5s@$3hA;%W@hWJH#qfK?UY7W+%25 z4Dv=w`xKw=XsqvhJ3W(2z^O~IUy-}jTVK2!?0cD7<@``(XG|wvx2;J-RQ-}`)kMnm z%VeaYX2#iNYW%FhZ6@aPFH+fj27 z)<@Ern$2ASbC#F7fn9Xmsr#w*P64~_6z=zKr^d&|T&~-jSm{{_8y8w@fGEA_*p{$c zaEO<(bAil+-JLz1w7oJH=ch;4p`oT)$EW)5VEm6N{x|Bj8}#EVb=$uxxzi6y{x|Bj z8~guW&5i#}%|XMw`PkHx1GFaPz{dAu5cqGy{GGb(o6dvAHUYLDu3Lj}&fj$YpPAe4 z8s9+Nrt;gwZ7V$2v~5e*v~5~eAt@i{&IU3TWC_C>5$HT!FVDWxnLRHVTloG8J!qqE zSs%sQ16SuCY1{s7g?^xI1MU0`WcPntmVcyeJ5QQw+H1Ox=qZ!r8PVOzS!#WU`tIGo z)3*8KV#Y?ji^Ye&tuK%1drGRJ>vM9Fb@`39ZQDt@9P7IS{i9d;8*ST-a(*LGyLMk7 z+5XkD$jklneL`5C$V+<=bzNMJnfkj|-%(|O zmVhBzt?EY3wqKtaHN&5rZJ^0{{WIY2`GK=7rV9Fl9=O9SZ&kDuxcVk|p^LkqO9&EL zLuI$+<~3RMKO>C4akkw!)$cglKt~bOe$dcAqaM)Nhc`cnuLW)u=q20VauxqF${&O< z3|Hxc8^*h`2tOf&d10r+G&iF;r|AYJ5BEXkE6W{$hH^suef@ZqFjed@9D*nnJgk{T zf|)fO!-ZwCFyI@MIE>V!@Ps5a(CgKvER*)ga3$3U4O#epoan?2MZnB5{+zlJwqcyn zY{IM_o?K6KqT($e`7CZ~Eb7ob$hqhxEYh6p_oOi~3vC8DEwRA75jH3}1h#lZ2xoVv z`El@x!*>ewqeuAN#rh$!bHif&2{J*f?7%(wq4)2-kJQbBT!T!|AZQiK5@|q?De_Ms^XG&705UswY)3b|ovR zAxHEvn+@*f((PpDqWeEZBBr?ILEz};*HRo7vDPZat85SdZv7PuFAj}rolTE@{a&(- zvEW#;pt#}vka=nJqtV9w^3k+1@DY9paDfOtO~w|2DD7ad|Hs^00QH$H{ld7rJA~lw z1b26r;BLV^xVyVsaF+xR?iL&ZBse5Ua3}ZA=A7M}By9FQ@BO~IR81AB+I@QH>FMe2 zU&|8=1mdSh9y1k!+WkF_UVnOXP>XWBzw7+WATG@1N zTE2DSdg^109ZNh_J%Nv_?>+Yv)U2Jh3&iW=hZx#Q{ziRBM^H=Xr(s}O+b`J}X+w-j zKtWWLFT982mLQI?Lm+_2$e8@(kDyK!m<-a_0>bhnaQB}v#-d4y%IX;uikUVH@Cyr~ zm&-wsMA{+MP0_ufp?PUnr{guK+x44qH-9qU&xnxQ6S(8SMYe1xEpePiBnhH*)j^PwAi!jNTIawUb z&HNRU8bQh_hXR~{(-W_zrcza8?W_jZo(XlblaR>1qTaQA7GaqJ6X$ew5i3>|e5{yD z1sbbZh&z{Q^--JhcVvFDDVhFlQ$EmsSU+6x|A|d${(sM=l>ZN#a&@tk z=g(Ha{^)uTf3*VEqq42}ex}aDL6^AQ8a`@M{*FUG*pv?^|FHfETYhO%GMB+{KgHwm z2VG_c9fA=A6^wI$Xa0pvsjA_=?TWTT7FaamNP3?8tlp7vKOVAqP{Ta@pfL?giL zhdyv+|#B?)z52f49FM%u2UiUj^S(ks|?-ratV- zd-I^sl~>5Va7Z0kOLI*ae{*0znU#-j^*gijVPg;L2WkAvtP}z$h;lw7_F;fXBm&~E zKfxovp#o@Y_3zOZD*$EiuzsNRM=<>#kG43xf@^(ftN3lS#f^Yps{EJ^#eP>_GKA9L z02F#gKOthxXcx%}KRqcQHWeQs)nPEPZXEQ~R}8AJSTgNE#3PQtREO}j^&nj)NkK52 zK44Nge(&V*32fux_~|J97kiqqO2@Edc)^Ls?BFeSSd^1D1UESxa_H_MVNi>STria5 z`6fu+qP8fo<=-H$`CpEYkrj#vgt4-?QysgI4~IYySkV zojknye{#@DH3Ksvo4!dnJ77+ilrgwsZz_^3BVbOK`IA-u8>av10)6X=`N-Y)oeOmLcGzsBv$LlY2sk5x-w&Ch1I@;#^vi_@LiXgw zbgMG#$ya%x$uETPK0R3P9O=MtN6CJ2fy{{FA3U5iwTzS`oNRT?IKu2y6y3PgbWN?e zl;<4)59vQ$tE6NCEc6r&G(zPB%Z$}e*(NDF+BnD*8R@B*x(R?%-_+zZlyCmhxLL=a z&eblq>ShVhB=;nCk|H_9En$)(dq^nR2O1OLpYmWaXkudKeZqPmN~rQmYXt?NAl}zJ zmm`xfrATxq-EPDD@N%W8>7w1mVNGnc<33H@|n1R91(*NI3yAqO(Mxy^}m?8OHL z8jElgwdh7OC2;7Qj_g*m=5pBBE?VKS0w&xOGZk3VtD`5J47WRf?=VLj0g51MmJ|$9 zv^X0jHjfc-9yuo;UVF92L^$0^tTlmJsYGC>8)P^z`({ik0Rq=h$2sv(jqk)AOM1Ap zPs(%}J#a1uC0-+3l>V)DrE{#f>$`JP-Zq2W{d5eYb1NNR21Im)$fI(Tc6@vg|78L8 zE_l|_o$9P)%j!f7*j19`wETjECnkc5KGCqE##xiQlst*tQTX6Mp17TTQV~OS^Wtx3 zZg|y+cahLd%m-|dYn2N@;W~vSnBXXzM?|II#2|;BhzqO7<$%6IkpYt=K{YVv6c-&N z>XBU(*THPqI($QmnZ2@?mk!tMQ9sfqpUj+Wtu8-)J{SRB>22Y*|KaAgLMZ7DHp-Kv zXb@(jnW8SDl!Np&Ya0?;o4-CVA*)wFZ6FDte+WW*JQUt74p;EknxfJHr!0RMUN?lf z2&x`(J1(fpXLO8f1RKX!b{X4XDcfI{s0B6VWLe8vyUbc?R^#L7pw=5Zl5;hU3xvcM zbzE$<1H;VWx3Qg;!8~lSxGul&r&+0MOy(4%+8WoeCBr-0mSQlMCN|i&BW;)NUQ32n zj$kcZfefqqOL?f4fw@&S$y%NW*9r`RhwXl>X!yj`KOAeu78_d7>q>`HT!b>y@T#uX zdO9#Q4DD&Ys4*u}-e9PfYw2X?9CkyR!TOct(^zQmIXQ*l#E@A7OD#4&%n@u&lA0lD z%;KrkDNC7G1R54jUv_HIw2Mbn8@nQ^nYt_bBAFFsH*g(a&}Cx8qpq?Kiki5QO2wXm zn>=Ztanu=;si>|Kt!qe%*#6vg9bhoRt_jUMI1=ja<|M&vAwY(joi*@DCeIF!?{GB# z!dk367Ib)R4(a)f3$vm32Za{W=#t5I=@aHATFFN!*DoT`L*t}E}b*nr@z7~ixX zrH-!%T#~;$Pqpt$5;L76TG#1aVQto~dKvnL8}Q%leBC({ALq>ziHUv!lfYO&hY$%Kp3X^PS~}(>@^g8ORQy{+(Q4GWNO?ZIQ$6-8|;|Pm%X7 zgdTOzs$B@?U!=#}zfRxRX{29i`VyWgt=-6=e}N9GsG)kZ$-dSf#vPn&KAUPa!OQrf1eN&ep<89CjAWa#pgMrnC-H|C*>J1#o!&HiY$KMEj=@)+1m4-r4;} z(d?I$7#;?I5Y{GO{e+YLJ&_+mSdYZ9wE-b4WdXEyji5jg#5)=Rc}k;By3c9N&*KVf z6+c@To9uQoygb}v+i!CyN^QDt)!3EPqc%pqLs6G_E2@Xo)-x+44<#}GT$$twg@>Z% zCB44dp~XkvljF=okL?Vb!sEJT%4ArI7!rx$E&K7YnWiltNX1cFT&WQ)k_ z>g0rRI0b0&SOXv>z)=C!!0kS*GJ@zrQ5e3{B89jPX#8;SU*cmZTWu{}#&4H!GKnYO z2zGpE_WWwWWwm2ouwOmJ_iJ=b?#>$mx1JEIg#r8?one+T+*+sm))HEtf>x z`?AKY+;op`XPrME?5OXF*?3;{44}^S9ae@V`VIu8iqU>rDPeCRn3DzzN6~9Ojyadl z;=ZW!P`_yqjRQ0glS&mnzmh4-W-1q$1qU<0ktP?k(s-L%k@=imIz2K8DaUhlh_k1|uaB)~ns1Tv3ZT@KL zNoUPwwvt1Amu>4}_umDfe!P=E1)&}h=zj&FetRo_3v536{qOIk`Qw-WTM+7jjQS33 z>7zlY-;wz#2=!+&!vpPy^$VZ#Cj{^RcVM&T6X_#CsOlFDBQ}5d`rkP8aL{&>a?{QL z&*qoF=I=Q4LlElWJhQbeUEECZ0@lD*$)9I0yi>rY+mC* z->b2xPO>asf-Pi;ULp-7J!KoDbcDY-t)Bu=k8bt50MvsdKCB^a!?If2UDqdwRnDOM$*KPi z*PPilM?T!MOhZLcz>3w;i#3=3gcFN5@IQq$g?eftlKb=`!bf=Z*M0pF zc>KvGlvi|Bkz=(qRgh!X>AjvR@Y1L*XkwubD5!NZ z#!8rH@{pag)Y&hTkSPV6VoBpSLYv6q zGhp&v#xU|T3wK7Bn5~KR2*5UTRq7SK^R^${Z2R!?@M7ib%Au+@DR)r~-iW@nwY5@! z*zP*xsKS$mj}R$-r0Ptfb|`*q5*v#0Fp~VM19%IASlw$ZGfMnjs?@TnM9H~s^x<}R zHhSbV+pu~d!5UVR6IufkY``E2Y$yh!4F0uJ7{*3Xb@EfH5sR#`-F84l@*{MCFYw^? zovEdBk^D8F@q8B;Q1K%kpV-G7g<%p6{-}|IBG{Jd!X&Ad_y1hesq7j`GV9)!rs+ z@~`L5oIRy;dJDqO!e61HK&3JDK%|9<1bj~GDKRh$&?>H!f9BFunTAi0u0TML>qH%G zL$$BX;xJvu6W~yYF#9aIv~a5=HnTm_6YunRZ!An~XeqBx=#6q}Vj!{(do9T8z58>! z6Tf2ti2=f+*HztZysoQRj`rtuv6>EP!Peq;lMYEQmDC!xW*ZQ>!WRz?C=Imq z?kb~Nqa%H}w8%$MW;t#77n7;ct(F)&!znGpCK^mz*~N#P@`jP0Lk6oU4!A96s-9in z1f6$%cDcWkczrIo(&SV9l(zq{Wc3Dl-1M_Q4um8dJl+r`Vy1S}>ZirmBbK!95j|hS zrWvj{9)}z1Szly`?Y||%wE%Q83-@Q=^1!K8F_c`wPmEFV9y-BPQBu=8+H8(Gd^{I8oP789 zB3F1ubZ26sUynGr&vg4?VoFVI=f1$N2u?2jI%{p|=Hubz#jB8+yiaZUS@~VjTfR@v zwNt*{ejFWoKaM~cfhSe51Kp-q3{pj2$(^R*L|styn#6HyF==hM>>XsZ7!L+jL#(Bj z>#=DGX)V#}X@|J+X5W{cf{!0q2+qmRibFya z8!kF+3_iEe9$RuykC{bTX$PNW7c-}n95i=-aMIPtFI{=LicZg0rPI_Bxz_5g7?xEW3lh!I_Zaq?bL@8C<{8{_<;yjQ^CXPdU)Z6(Tna91I2ikT&8&khD>mTuU z{y&4<_1#V=AMtiPbvzVmh-%!=@vPZW<(J+Ls2`Q! zolM;nC};%8aekit@ktzr^wf{u4q*Cj;j^HtCM@tk*Xx&QYFDHh*I*EZe?ji@+b|X1 zX1IM5=pTo<{^agF`gi`@`S|AUJQ((WYveo-3PZ@Pe-v{6J)wV7{L|feAo`d`@t^L_ z@x%08>_4aPn)k1CgnoD050mWtHO`wK^?3gI=4e^|%H;F|?C&2=-|Yi#ie1@U==9y} zY!6(aJRoQ?4^nvWc+gBaW4`4V|N6jw@^~KI>USQ`BaHjYDDwdZci)qyc>oKia1ay{8kvJD+tMme z0F$VLpnJeNDPS#;fW}A-<*?Oo3YlSOvs|HtJR_eL4Ppb}?_C?Y`duW@5AZu8pRy9% zAMm?tZ7y5oAKiwPfxFulKoGw}f*)jxGrlu?>Az(mq4G~g|DuD$cEvE z*PE|Mt0nyIZa#?OlbhJgN7ER7FN*(4V|b84)kh@pAEhzO)&0Mi#=ua=!p6#G^1n=D zuu?$&j_D7=t3nxD`Cl^~5dHjCN&R=S`%jn-kX!Kx(6CgjSPnoZ^MT$3-I3Mwl=6>+ zd?0V1!P6Fg_{W&rnbNh zoyPE|aHG!zaKfZe9AI7H|0wU0h8r27p@~RcSc5-{n+kiPOhp|-HD;6$92DhdE8(SJ z5%%1~LRPZoxtD^qL8y+kmn2nkyrr$UhpkUp8Ep5zYJ|2HRljc^@xb(|uddr50EHX= ztK5;_Ncx{)CYJxzJk?@`G}}MssRGfX#^`~-F!bW)P_&+&(FDtx+gs?A23~nn0GVG# zL+~3!m~qnkN1BAO3)g98Nu$!2QTWRkp$-hn$&Q4|jfu;Q(G84`Q0ucK49b*;(2r8f zN=rq`(a;V?k4uNr501#tK2-hcO|ov#Qc8|cj}6L2KUB3LGb0Q{p3;pYAkZM5)1J!O zT@1)DM@V9hjgExVOVUb=j7Z6Z(#z6Dj1QZ#Yzz+2&<)a;2JEoFQ`?=4OEE$rOtPL0 zune9Kj*ieHZh&YYPW{nZ&8(5B+PbOc`(m93-2&PoYVZZn_w*^sFH%Wj0Yc_SOqI(}$F&~8uG23Vnh-n7 zTzAuEv-0v}Yt-TX{imA#Z5+~q+yoERs+qGO)htCCk&gYVSn3PU&YReaw>N7a_U61j zDK{9e8~w)$ww+Y!M*OU&(LUoH$8^9WQQFx%!$b=Kv3Lqhpk?&A0|DWan(+k@db0~u zb9(M4s%gBXO)2uUhh^vVNdFEH?G^p6cKDdeRI%P+L($o996=NDll%-cPbUr|n1Q?Cfug!m%i#%uD<<@l55?Mn zlM9%W7GYLpY7;cIha{jmkfVo6in-fkVjL>usj|Y03?w*?qFAUFWE1R@3!2}-><(LC zW2*{X-O4ZQxbUiJlA4O?sL}*^ z(+4_24Dxf6NXyeVkU?lCkijOeIS*&bKIk?M6T)K?2C*kvfTnKDC*daxR%IsbXF_yg zyohjm33WMlkhJ+l+Kc~G50tBC5SP#^%Jm3R8&H+XH>lsG(`B9W65*y{vEq^>*!0-f zvpTJ!yF6=?fu|yCpYh_x`ORX}LaX>QH?EVz+)d;;%_1}1Z3Z6Ns*9AH`$Mle4(@ow z{oEO^IZn`e-7UcXrE{K!dUk1al8IS>_B|J zK#hmk7u-{n;l|ZgP#wYQvy3ZOqs_L6&~!i_WsrSdOTG8kBG+t{m;eRhCE7Wct?rbf zI|cPa0Cjy2$*uRqDM5el#PkdM5db3Ti$yU}0qLRmHnLh4|^W*km)jvDyp9a(Mau?~cT4tczKD z)oIrp6#Q>$;c5hJut-T@JaphN%tzPXXRCBKw&%~_kEPvv+I@J!6=UesO$iN2WTu*O4sxQx%VK_Nw@q792@rlL^^FKBm&I7390%82ta7j!>Je;biMqU^pKQgb!e zAkc%}0%oP7^KKx-Bg^Y%h%f#2cAK!k^egCJF2uuYW{G*&bo<}x)(89dn{Iu?h8R`< zmu@{_pS@_9{JAE-k0tjA=Z^R*!k_#_JpNeMCQEsS|XP5-;nqsj$sF?g8*@m=62 zz>wBjt6-1?k}9usb^<~3*aP@Q{7?fW(aOV4<)H&pm$6-1kK~j|6-6aYOt)TsXua4P zZN-khuXo)U+|@+ZFcar`p;qcVN)?Hl_2lER$5*j8PmlL~&$6rT*>Af$?DWy{vtcIO zUM$hI>(k7}NUbu!MKc#v4w^x0tkky=wPa~mq|J79On5X0C@!oDb0!l?8Znhz1^6EK z3R7jXDt43Cos?)vNRpmN?DxTD-&0N;ud0c)lWO7>E-h%hujh8@a;6HJQ?((2I@aU| zV)KVk=Y8q0UL})tlh%IBxq1NGShpAuiXj!Ry5RggmHafUSp$U#iOt+s%rtf#c66~a zII_Td3jUGfMWS7oe?u`}ysqsdCigNc6?Lx!rXQGHcw<$VKBVnsF zp9$=#Y}E>%sLrz%g$VEQh(>!>(Ld>DeL-EzRoAC`J_2LUldqTvCa{Qpv47Idq2aVg zS{IN(0fx*bXOzxZt?ks@E9nw^3NeHug2=`5x=1B*HP9gvoJ7cOn_UHws#yZ(ND&iDF42ePLcKD3uyZlBKQX}23f{j7})&yN(LJhOAwfpnsQsw<@1XeUaxG>GR0X_x{oNB|lk zd9gW)TPaBa*(-6g^ z(|&o?I(d#E)z*;cTfAcvT2o?oTLDy!5ew|3klz1RO65q8!p3Nj1|*9qyl_+2tkuJa4U4u zJ+#}g-n~RSy||wc!~5`NlZk^SEB8I0%Ss)-G7Q2Rbhj}dgp~{@NPr8_Gq~vLNf)5$ z#4i=)U&U-CdgTV21qtIV1Y|6Cl?8Dn&6=7VOXby=ngq!*M~s*bi~UQp$!5Ap8$L}Z ze$);TQ9telItJ=~ig93EqO6UtY@Sp_jd#5O0dz8(m?!1BI|bf-8xU<=BmG5@sC&s! zptrf?MVtpd%PTi0r^Jg-(`C3WSLb{--p8N6J`RBX1i1djhW!C>eI!u%7rh6Y84%L= z^XDhP)zQg?QQBEkLeWaj-dN7nRzXzULgHUZcn|LPBVodC;jP~Su8(~E`|$Ta0$kZv zyqn(vYDoD3pzBXbcz;jip8>9s%9hf2Drh=+0pcu+1-+X2z6z+yCR{fG%Rvc>k|m_A zpB78s?JlRbv?Py(H!*iiPUK-wU|>>)FUq6%slHG$OfQ5cdyO=iGHn)1CRr*{BI5!6 z6~{R!%*EOHy=LSQS-kYv=bHnL4^6n+0gw(g5G*^k&+N}DF1$&WT zPV5kk9MyF5t##jLlUv~E5BoeuVm(OXLgsaapKJ$SLs5V`CPW9@`*PUHTF8V24awM=+sSiV;B(f83 zJ4UR+IcVsjIWVLM)}(XbR?@TAd&=5k9^zB)wHhkutt?SKsi=RbWKo@Cv9Xk1L{5dC zhxhkzN)5z=N@+`_TziG>{+>OF2o!6*nv+t{ve zA^p8Q6yMzXGkQDiEzU177i}f@&I>&sW4gMTBUv^@?TX;FulFj~x|b=tvVL*2CvQ=! zD7o^rZ{s2xqs__f)9TghUKcT9!b~|W5c<0&p2MO__P5VC>au8hhlaGHrpt_+`+!MB zjkF}Ld1CGj-LM2uYDMd-lWW0+z)QT1kpf8~kr_jQGd4Suq!L3x z20AA7b23NpL-`}$a~d*!s;>2rW|5wxd8^&m{YBZ-)D$`qn-$#=`OQc^6tQOgxj$(P zF$$Qld~Mmi#g4*s-uJ`+F4{YgHPR z_*!uGFL%l#$-36p@4nVlJ9m0BF}!3N579q2eV!|z(jzrRS!+)dC589W(!BazFHIoT zG69R&;h^vgaSVLcm5lb`-TEW z14=6&dEUQ?+ykM@BJQ0>QNZ65`ZvWtQNRbHkM&FWPZSVPc+20b@h1vca@=40{JWs{ z!{%<*6k;Dm0sr~tzAwD}yZ!x;_t-fY1suB2yzt~z;EET9h7QE5qd>s?Afyhn`Qsy` zzd5jSilAR6$7ywu$XhA?t_{&GgU9CScQBUc?-9g{L6zWM0$o<5`pT?ByHzO}Vh|$K9 zA|c(M>vtkAEjcI`1jSq=xH%=iA;pw?ohm1!eH<@BUqu7nrOyL0V-#Lk0w~JugG#)B zY$^bu!U95f1cFZD2Pfw@!!T}?fFRmV6sXukC-bb&DsAJtU&?|ryg#Xe|xhN%tpX0s_Bn`VN`l%5pt+t-wE3TZ)=tz!;k)3 z-u?v-Y6AHBu>NqPBQVwV%53+!6NY;=}Oc5nO?@r|v zOe)=A3p285#r7$Ap*c|PI1qHSazBf-;Jgx|w^+n9B*2MPAm*h$FtUDtg6IrLR*4-H z7PPE94P78)%msv*jcAMl`MW$Tq_kx)BPHm7r-tCU8ea(k-8w-qfZkuL5qw`F`oLUz zDPIq0fYAQ3^!fLK`!D$FL1rK0?LUF905bdU?yDUjTGCg&c$V(cb#-Z-U zC2o{gBHU4wSKy@g;3aN4bSvEQp?PZ~x7=hx%wAA@m}ET`W`!@T`jSDNQM4um#|6hj zc;2eh*HCcyS9EpJtc(bQmDl~NO{)smb2~3{1$!z>O<8k02uyigty%u@i3rq!+(r_{ zH$HO{55s^5Iz}d!rYHSUX#o}L9JGn(;^?M=&w3+{CngvX0a%KpjNP#Gs05|+F#V7W z&AZ`IIf+esdw7IULeMFUSnIzv-yEB022oN@d`~R&#GyREo>jIem zu>My9{l*Fc_}(?d9yZ<;v@PM*=nuK}dPpLml$V81$O_LZAz{WO9-)AK^~RsYSn!!N zW=>kG#_6PD4pr)_KlBEmyKK3Yz~>EcSQ8{3jOscZ%Yl z3A6dXh1n-1_5YQ=>)0t8nkmoI9{R3-A}vA}Dppn{Qpp$uv|Y#l=WW*ohIhB$f5-F( zTcD)D>GVN^EdL6N{SDLq#A3hI@jvp^{x0SJ&cc9Y>su=`9TTEXI5fR5c7B!1$nfqW z77kYWnXX$tG#{2*F;F5#QI<>;z5LWQ_;)?82H6xaE%K1lT-g@Ll6-%O$R_^J5LsAR zXE^bZ*-->a+SBv8fnz3$NP1~|v|l2!AOA#T;RZ?^u_y^T3Z{>3*1O)^X3{dJnHT(DtwFE?kNI?Ke8TnSMSU3^rF5Cyq8;^92xxRHFljHDUc?DmA)-CR= zCDrZl!sG#;UGK07^Sl_P+is{$Qbfdg6;`7~C%wIg=XZQ`U-bcF1CaZ?xy=5R%WnOU z(GjT8rFm(6*Dy%ubp>`e&E}q*`4bd*hS;@iS9=^UF-9F!qt>7$LCURX($7UHmLYZJ zo(i%kD(W( zq@)7P6g&%bDl8hGcA1jZ$Gzu2jyeb)syk$<*{P4U(SN5AfIM%%P6+b0OE(+7{VqG3 zkVw-#n*!(R6^GK)7E7Ix%x5=T_op%+d$}49qcIVyK8leu^X*UqX5tzby`}2wj2(Xx z`_?}=`4qLu-ddciB5^-AujUyGZ#hDl5EZ3_7ZB_v$33l9zY1fRyI0_0H^`fy-fcUA zV`+uDhRtI?1sWVvVeFHx6Rgm+9f@u_OH<($^N!oBmN?DYxWUs9PR_i-{{G3`Y2Jny zMNGMkp%BGntw% z_`S|acJkXqm?9D4tVsRx!A~@>U%Qnk6j>+kRlBfo(rd#IQ<6A71YZq-VI8akk}Fz0 zD9?z@>@1iqq$FR&@VA??DNXkG8}j!BNxC=&qFALVX(gGOv$P?w;i=;DLRJpF8qyBj z5EInA{a=kQvb|;SJ_zxY@){$(4JQ3!R;6~CdC~6R3uSX62LIf9v@1x!b+sHnR1UQf zA0A^ZR&X7CKat*>{tP1agUDuo)cc?kbghKc4C5*DnJN=oFiftq;F1tJPgz~g_WIL> zKttV2l&JWDN~$cqgRi{avhN0c5+YD=#<1?_O5muhxq;Tnk-Ygz^8}2CW~I`Mw3m2A0@GFNqAl|{2@%AO2;Y!&oJdloMb8+rn)yu7Q;U&g@ha%ME>!B6^J7uEZ- zs?w)tsV#MEe#7jH3|;o9GrfXSMdVm^Qjf}; zQNcspNWZrfd`?$t&r68h_h}n-Wi94BJVs3MVufKZ=3Ln z?v&)Rm?_i?{H{YPe#+|w(Y{ zy%7~}aqfuH*q|qN+q`+i3)~b0!iNGz(X)B_>?lqB@+v-bNp;1a6TNw}6c)uTI%EZV z6clauG+`leXd2`f0%x^0%gR>Jj_Ukf2#*g6srIt72ho#R!}m_X+4*es-K3n3F+>R` zyX4uu>packNHt5C#E)dQYZmI!NT3uK)*|%Pp|E>j?5sxUm=>1sSZ1xL0BO(Va_SbD@4WX zu;8NI5nXe|ESt>)K&@CH6!n_S7svUHC>XDfM=m8Sdbl5DHd^`j~-w@DPoAvtsZHjsj!<(W(f8jZmLXruO< zYPIkKJ3b~N5PL9(WUwe$W^?tM079uo;p%3N1E<(^3%Zz8SSBy)Bd0F>^+s6asHd`< zoh8v%Zp4^Sa$<-sPmlTe`nx5R^B@Q0Ah2wZ`koaZtTj3lSF^QB@BzA;lv1eUZ24|~ zS@QDseq%_FBQB=x_jNO}(gx4-lVvV_Y_6KFA5{e-Tw?;4wcUrI2{-PRC|!gXc9LL4 z`CeI0=F88xcAsbjuG#q^K6Eb?e)-_+g83=wyqZ*sgP_ML5EAaXh z=MpHs)VL#P$AW^;zhC`eNL9Kmfmy9KhFEf`u=q(GL(*t#-_yfFO4D6MWHzy~9W7ZA zggG|%T$wlZTtTV_mOOnS=|#lP-D7GZA%UPP!P8NK-z`e{wQ5k2T(co>RyR6DF0XQ@ z!g0w#)X?H%aB)99Yu#`py3Zf?=c%L{hn3nk36$vKqRlI@tYdL`{B`gkx`cJ)$}-~oX& z3F804T=^@@;SuY66x-Vhb?&O|r3-I==sF4bBofY!0vm;$1^H`K$jKmGWHV!7V#CRe zA=YzJj@9|j?Rq1Yie9}6VO&Yy%H?RqlI4P9ITe-XukJhN1J2j08kC2qNHn3HmV4&6 zjL~GCx9q=kIb1O9H+`=3^eAs?+vV`)tex<%{ctn)1NnP3Zf;aHC5wqSPkC4~vm{8J z`)V1%*30n6J*hbP+77u0?@zUPMV53LKER8UEphqMk0@oSp=MrgxpvH0arYl$ci{aHK~1^jsuZ%M@ifhDykDUp8zcxF}B5?T(bQ@9dwQ zUBC3`B+z|3Cl>tm@IYv>Q=Mu{g-}UJ!-k1x6@TaG>~j9tt`N&JX!S|_?p|}k^<}zACF(`$8ZZqRBD`s@?W)}_bIEALr8yqd{L=9+a<^j{erLBo-;EGg#&kqtF| z(I$x0r9Q?;079nT)5j%-u)45JpC5lYVxGYl3`%Mv_pxq|b`K5o2@)3;CS?1^E(Zc` zEkVZUGY54yXm8c@8}Bblo?W-dpSSKk51+rK3B`CidtSPr-eqmdfb(@2!qo7dBt?dO zinS~Sldtg>ma1?R4fQzE%mUD?ro2KeOnh22PZOj(sF7$jOM%r8DYEdaJSk1@{1BVn zC%%NRT4>6&ufO5xeDFqbB|%i-WDXS_!{M58pjOGivT1n#g5jQ{SP}E0G7U5CXlW{? zzDCc4c7Z9AU1G{>({@|YCqYiMnJCF(r3J50J|RnV5S!A??gC~e7H_sMa&nH`IxoGG zY!0ehOei%qRGMNEoUHB9_g^hHvl4WoONP?O4((Ubwn3_P+{S4^-pf#VSx9c!{HZy= z+aOV7%1jV{PEix1wh5V+O!t~&-eM@Mwtd1%dkKqL>(Z;J^M+w% zmEcd^4r0{q$t7lZR9sJ$bLixsCk^{%<*7u7n`_ev%&u+3pLp{cqmjRl-@VMOi71UK zDTm5aN!D({3%RCHA&7Xy_4c4BZeZY^@gs#0ty|(OdK{4!Nx-y{w#%7vMJ1%^@td zQ)5D+V|uLxS(j64j9};fhSRwow&VNHN(Vxg{!D+t@ub77<1uA+Git@ZlF09(jWR%KHV500sQ|IS?4|{Rp8fiC}9d zq5vC^9+V7%LJS|dlacW&Scn7g{%iE<16cJK(n%K4oGEk+$*waOYAu^7+ZU#GSS-A7 z&9M%9KIBrzbS~zg`z_`Un}Q~Yvcb{J_ynR-5V~;t9;TVgd8h2RfmSot_s`!#t4eIX zbF82bp?Qf%U}{gGL^$N3ZqLUK=7_^wK()c0cTQ=B#;jOm za2s|x_!6%|`2)KyIR}uB(UC?lU7nQF2LGyHrbirNlao1+{xGe|JdqQl}Mgv3K zteTgaMrV9+g6RHIKab2i(@#Q3iPAp&5n~vBOb4P$Ba?>vfa^$-a)pqwhe=HT^xlDnBYzF_lz;>1Lt`QIE_4F@}1dQai zTOPJggwO-zMd4?_G;|$AJ*?_JKF4+siNuGI-AW)9!CW;LU^C?LZ^2p``#6PxmyB=6 zVQ{siogFHdQ8Cw(;noyN`aIlrC_W>uwsM%b2y3bHBRZ_CO1M@DlgsRDDEuf`>9GvJ zAL)#-Ixi3hiv`4pSVpZ$ z2Sax5XE;vmR{hQr90vB%k-)EWt?V?H8SSrTB?6FdtWyrTTZU2+)88pfMR;!}u#V1; zeI)kZqxJ)AfR#CkC}}+15BgO&R+N+)y79=%;0y8P#exduF)g3gL~(7p>dF*CChHb#f`@dZ41! z=rG_?<5j*+dsJ+og-h)p_JS|)9qZf4ij+1`B5Oe-R!v72>RzcA#xWP{tGqkT&E)d! z;jc-D^mOMjcU0%wYM4?+M~WFyKv2&xUS%xADU7xG=%gpWwVK?q7XpWou?%Xf?`@+|pqpEEtoaQC}zDf&4p#JxZh zftNLeQ`6HyEoPqpJMfKIrIpj2Mq^mIutVb3Xw`KQ+*-bCus!VHC;YI+W0w4uo}n6i zbGdp%MXA_WnW3=DJ;&RcYS&M%Z9_H6e#@`>?w%;WNO3Objf}5@!+hN}&`_1BjC22Z zNbjafi&X?K#nFuki-jKT0rmMrm<$@&&9%p}sJ9iUy>eCP`G(PTvyl~)+r8MEf`D=*D(UIa+L`<1+O5`EZf%v=I}e2O|d+(l7s zWU(EMgmtfhmQk=Bcy(@tFOxW@X`Sir1yeRh12+bD1XtA~Pm7^r5x@!wyij&Wp!+Ax zDDXa=(5?{D*$LQ~5vhw&O$u9yUPW1$o2k6B1tnQkJ@t43oh|AL)uuKgpjR8B)ZC;q z(sMgBl#Ck$mzEB32?&?6RDidHZ|7)Eiq z;V&r=!#k}?0+uKk$hN}eK>pIj*_1hn#%REHYm8@`Ydh=Z*d7NvDBrebdN4V&fk4C& z{ooa}TP+b>VYoSr4t+iCl1uf)Mcd8roE!=tFQ*th{{dTO3p*!qnMjGPs;5wFSRNCF zX$}}exGUTN8lT5WFCu~brKYvFH*)Ta{*xnQ4Jx#mCmMrA;YruPA~h$RJ`O5`KBa3hW@qGA==}=Nw7MO~)ds@}5yqxjD%Xp{-g8MY1y0 z?Z4IKU;{-sC9eX9)iu#Iz6F}7LpwoUCM|wKnT!}IWuc&IB3L7zg6QIMR72NJ-jiRJ zzEH;O4K6Nv%Kc=!U<7U|Zyb@zo{n5D6sG$V{bK(naktqxK3}zm&i+AE@ReCgbnyCe zsCodV!R-S3RB9dxrg8+V{>kpH@^yL0KmhUP)R43!z8tlnWa0|)iK0My0yrggsGy`c zW-ppYaQ9f^A5)m z#k-u_yy5IrIRO)`kqlVum?t9Au*}kR?Rp<5KO*EfyhH0%q|(al_dJYUTM-$cSO%gD|3mp3I0% zxa2%<5!Vi+s$JmZ35!@YCob|#lN??9UN1lS74ZHE3s^=%cUxe<*9<;aoX$6fzFA5P zI>G`jo${Kj$90pSXfj_n^bj}^NDK37-FYR^o@{_{J%~pp#6TK8Ks)XrfN{;AziISPYnK~F! zIz$>iQ~(t$g0-l1%v}V|stjBg`dMpXG}RwZHj}VBDZ*}$F`iZrk`?5GOH{>PDN^8` zJlb{w8EhTek^Sxn0adZH{8K!bv$xK9di_f(CyJ<|yKt{A6r>=u0(2K>BkUX;9FS1A z2{U$ro`$2Xh|nhTqI~5|4jC0-|NJ2Y&7NFqKaKij7mr;%w3!N53GyWw##IsPVhb5? zYNYgLjF?Cxaz<9?`;rf*8-b|Um+~G<5z0_$Q&LpKN499;#Ao}{0fmm%QE;PNH#0Ad zHcq~nZl5_?D=I6f!nkWqs>Y5Nxzh5f9U12&5ddE;noIVBb=nL-;(zwv84|d9o@tGBL#|G~Ug7V16vV{Um+(L3_g6PER)OXz0^}9pi3e-qbLz%dZ$8BWV zHw-FJS!vX277-fhFIOC=VS4KAlV#NCmaCz6s?(WG=2SsXc`V1&HMSQzB2A>5@ls@w zxmnrWOZ!s1RM};bZ-YEK3UM)uUidr-gvZ_K!-%g`mBs{o(psySai!KbsFuuwy)bzX z$9>QuV&tiJOV)_9ht0zA$GqBFP7zCf#gSN4{{e2GEqt1kxhI(rfxQf~D19s-hkC36 zJaurID0m%cp4zw-VDzH!fPeI{1htwQTTChVCxOWs;sn1LmR$co%)Mn)oXfT~4Bd1$ z?iSqL-QC@jK!C;_f`s7i5*iPh;2Hu12m~iM1PksV4Fq=!_L05sId|W)@4iRI`{QF! zqv;=XJ+-Q8^{Q2K%{hVb@aDto^Nxj!!0A%+d9;I|)YY{*X|u>+Q1zQKBpi1}uG`m( z@Zr4!zF5O@KFq%GZ46{)Nw=$EUAp%AmNULj>4vQ(h|*nqqmAuS&!r0hBFqH})8*|9 z@{-rr^$;ZwBl$A^#(<2eXYafh97;eD^)FS=_S5;tlvpY6e9qqSu-1ccd!SQs$5kg+ zwmh58CK;dJ<{-BkG@(o}aLU2wi?5ksrHoE{%_!Q-zdZLAeI)3XY! z0xx*7Qi~Ban{>)AJ{)bQ{(!z z;P0`sy5eICBn)#58GjUZ&U2Q0-e{PuH*4K9TImIg&yot%C+6z3@5v}WFlNYJ7%q%V ztA?XdA~o8rF1C!E+i&6-8y@VW=(q<$-E_4u<1 zI4x1VmF_{kq96hJ%%xy`p3w{0`eerMi_BW@F;3H4^5s>kO=9}j3U8+wKj_~X2OGs0 z($gl*Yu{o&4Sj1*NjnNm1DqN^F}+s5Kj%vDtuy!iG0DVoh6vox|I~%@!^IY1HznND z%V+3?lw)ZD;@A*60wH}-&yJih?uR&7gA^U)v_(6-N}O(}iJaskWbCooo{r`+faehqbn z7w*>oML8z*FsFY3k^6l#|CEOIFFyT`(9d7mW`EJi@9XqeCsz$*8YmCNS*;K4PYC$` zyOZBxjl6%O9Q)PXzp5v)v2A6;K)*rXn@&e))3*tDx>Y8BMCb$d0Uv?E5$*GXI_9s;6S)6|R{bkeYdpD<{&##9xDC>*j|#hD0gL zOAExK1@gMn;_-fdU>cXoN(!Rr|AS^{s2QhR{#z#AcBT)76XRo0Ts69OHePyudI?T$ zL4LwOY-#le+zhJ<`$*J7#j`Ryi|Vi|AKNlQYA{lWiMfVG0MG1|Ploq^a1nEv&k=ow zQ~_#`k8Z5aNs?}B@SRGW<_IJ+@$m%8xXFV|8Lr`vZ3p<1q*UVUgEw$%$L-A>1H?%y1ng(a)mztyq;1_X9*PN9JPQT%fy7wuC&w=riOg(RlQvpY9_@AdzB0p2{#3S^ zx%{O|OH0>2kHI3TN{MM7hcY??v;wK6(imQ#WUso~H?u>>`XMa$#am z%?GbP^M#1V92uk>m7oo>M0)AJivA!#uzdJD=Tn@+*)&&Mk=kAJu>svAo1N~w{Yw*O zhRNf$u+I9G*y(5JD?}dKDU$=KStI%>FY3P}R~;=KnGBq_jZkcH>N1|+6G(FA{nR^L z*?Yt9nse_-&K%`my*X={@O=7=p8p`VG53r?rF7I$S8}jryy<@T=iI~x{Xq2qztr{V z&n1-k?M9MUY2`k)M%ykQf{y)i=owZvhF#LdKe&x>em=b2zuKwvN|1xwNFbM{POz3{ zaJVt}Ewy~I(G=JSpHp$QHCf!`Y`Ze88cZ8;qIt9A>Hj9JZnS>3wA|veANB3a-g#-` zzLgoF=I}kB8ax3|goAsTo!!M~C- zBR5N6s#5o}w03i&)ob77%}z32c1f(01n-V!m2r>qX!{~M`u#mG?~P-q^`L2T4gGi1 zcAGVgrS^dPeZ-CU#qRt;FKGS<`hRaLn-sNip9qdVi2foC~jzLvA8D_x`Fkn z8&;d^>6^^=4?3v7#2e7H#zdM@en)rw?%e*-b^ZxC{}+DezmW6))Onsg6rKLvFF*Y8 zC*=HJe*PcbXTU!rGx{|Q^w=xgzr=C9I;b_MG@novCxS=Pv z$%}F7w+~sA*sl|RW``;!dW26*Y;Y9`Uyp$NT;D9AXDIra_IHubAFIYr$bSIi{4!_2 zt?gy`gDD#yn%qB1$o#W${<%Vc`|sZxzpNbm(EgP{|L;s;Wn7$pRV85hZ%e)^(YF*7 z{L;F&^>q$rDNu(Mwog_4UG#Mvm8;;tMPFxo;h+95Ql>xQ=l{}9e?znX8!TELLY2JC z50mk~ilP4Vj{er;-|_Q*K?e03Hto;&`LHI6f`NO{x7=4CwXf-FSgL0Q!G}Mr+;5+WO9{>Z!E8!V$Fz{VVxb#aq_D0QCLc6g7XOdHzSp&=mf{rh$| z=dgBm`i)1&@4@K5+CLcni+-odXDRHZy0YHsX+xt}h&YN5i+EHi5wnuCmKOH=p`;vJ zu7Tbq<$9aZ+#7<{Yb=UWv=~Y1OooQHdZ(*0=P)wQi`EQ5UYD0|TCMk}YN><1c+P+l z4j`|z!eEQ@6MG$X;0i%bESCk%3jK-fVpI-0sx{;9x9;l(ucR1=F2ei=Tafi0E4?ur zQVuXcZJW9~%8nuHYZaJ}FiW*#GJZ24ArEWI3y*kG2hnMw>EG!b)lJV54vou8SA6^` zOh!;BE3W0}YYq4Fa23b7Ez!Z_2NUASjp)_iNWNiy5r zEh+0{4A7tA&KtrjGb9>o$4wfwF)aSXPDd6H+n%-si`DM85Zp}p7P9R6xB)pY*N)p- z*A$R)iz(nSkXRd4fdH2<9j>EFO^j9{lR#ECP7GgjtlHclEHgq$yGt=$Ow$m~)lXZ48(vu9oW$1JwAZ;hLjy@5}E#q66 z{W)`78-oD+EC}Wr(?QVvRWt$(3ehE>a{GgS{pC&&vnD?Ltsdt;cw)=HgY5q=fAX*L z^3}#5g(Ae7nEcKrXXilJT9<;##E7RNTK&VS1TG`YN{#>B_yL9QM96Ri_MvamN`5{6 z_7oGQOplFOK^|uP4U&PRKd^CZwwzC#;^7dPUE-ru=QW-f$*2qw9amtX-d|UwO^~^&r9Oi5nKS)fC&Nn zLsX7H_8R8dAPWE+&ya5hKcrlB~ZGyUu zCz03Cj>{tO7{cVadn1r2NCGk7mO*+68OX+a`Xck__Yl3qFVg5Aedh^wT$0m+33C4< zGuS`)PRsw(cm8IE|5{}KV_ReyCiQo8bX0Q8UB`ug&rmsue;3HkS%J*ON2X>r68X{QmJeRWLCEgo(0pMUpDBA0!EBL|PJ2U)S(bYhp_fs#5KkT3N zhZ$DlXa;pZ+?j2mARtKp+g}TIZl*k@YA-CToUMeo9-!uyo|eCDf8&R#{?-0ry7>oX zgNPAukveksqn#ESoEAo`KYZ(bc>V2Hw8?jIsu|T7?G-_RUwX&e2I(eVA{U}3yi|>< zS|w|UIY0z1{cLVc@zbyH&?=X!7tx!s(P{zJc+OSa8$5Kkc zQpVa++Q3rQ6E39zmvM$mo4{oQ@TH{jWz6uUwee+LQ>2trWb9I;pQgxq>q*J$$yn)0 z>+8wBn3PhVlyRDrHlCFA=aZ7v-OSm&kFtA&v-?2Ly6K!ua!#S`n{73)K5>1J)|C2i>uZRvyfbu;?)68ZHA`t@PmbaUSHQr`4P zhGvXgI#aV1hS!b}_I4Qxp}F9&<;&Oh6ZYgA^2IuHvlaK$&Jy;m8H&Zaps?l9*LJ6M zCmHgnIy18s71d6r^-dUysJc92E3m2^PV1>P6c}W7 zjxpqBb*8T@Os$oVe z_!gtKQ=mIyo{Q9(*0C_Yc3hyhcV39p1>vLr+A((%bC})_)G1aK;Oo^Se6T# zV;*~LPeXUwJdcqxt7Fl-+Np-#>3I<&7gEOpr`j(KJq_~$3(lgBC403?4SnbH5(_Sn z-M8empFO)@&2x)7Gwv4V)PD8s9hnyvbs^f#H?AG>?5UdPZ*~^kEncc!@a)^47jJgK z+RYQM?K|(zo#*v+=G-l6t(`gVU7i>9b)noX@UI;`?`fMCympq{ExD~-Iq$ohmkd$R z@rZybMVQ-0m_3QG@KS)vDVSR-nCU54c=STmdd(es&7SpI_%T3b7|hKX%ybwm+=`(p z#pd?KW`@NUKGskLYjbOBGXrZ2&kd-?hPm^GnaPGl03lSG(AJVj8MGZSFK}W;|`-F94MlFt-3#qX6epq#b)V96Qq-d#2H? zNgJ$08*Ct+mW-a(M4ncHo;FzLmYnC-l;>8G=bH$aa4O6VY|M2o%uVE1aHdxq#IM$c zUTq?2!09wL@HN)?G&a#k;A|rsWFzZhBbxvYI1R@JF2_0#$0lkOoTX}mq-tHHY7^`X zXK>yibY2&5-o)I8bL?+W?5|7gZz58_sUP%IDAu_tHc@in%()wna@U1(H$f(FdXo(T zlXZTRO^jtY`|<|)^1Ar)CQt%SE3ttmvCb>8iPi>ZZQCGiTNiEHgap7D12%{P)&&DL zvF_lUcN>&<>ypw8DsJIgO5r-T;o48ab-m=Z$(+bsTAqh7ik+7>H1h{DOl-PTWK3u>3XhfX{_rwuWOsE>jn^LNfYRp z5ol`@=(?tADW~e#rD{J-)%DiblGoR<(%074*L^Xir9P$OG^K4krR&eHCCjg40V<6K zOzBD67jM|-rP&uvqZ*Lb>xYd|^QJLSOL3W2`eh&NBnbGkwXk83c?;DvW7t zj43XR8RYm$rub>%_$i_I86@>dI`wIM^(j8}8T2ocY+t6yzD$XInE|j*(y&kCvQP1_ z&!E1WWO+AD@@`7x-3-`ilEG=3&}mA*X$Es|l4Eb0VsA=fZw8TklA3%PhkS~gdl6`5Kd}&I2X$B}hNh>~$CqBh1K7-ae$=W(i z+BzlLIs@^aWb~gV@}Cm)pTWAFi|bAU-7t~ zqNl)zFJYfsthn!ZmZI;wk66M1idY`wad&QavJa2uKC@U+$?;@v?^hoY&4Wi`1vbaS zxji3z1V;CT#Y#4h7jyegd?ZE>KrL^Hj=M~{V|}-(~l=idWU?3IS&Y0@*f`$ zn)Foo@W0>ZZz-NRo;T^+@ezN2fYFjCc-%{|sxCH`s;<#@DX)6 zAa5ync|5Y*)9fR7urJpQ_q27V|Ku^~OJJM&Y- z^nlc_z~%T$ThGUz0xSEXekBLTOKp7@*NBooB5$fGj|u{Y6RuGtw@QBA0`G1Y-uO(o z-;8!CNqYVfa`P*E;IR&0uzHA!-afSd7vaP2VbWiO5C0=tTupjImiAgV({T48NP3<0 z4rypU;$}A`C4}DD=WAr2+fKQJXs2*M)lN0%`C&UY*xhF z&Be#RPzqW~4l(037=I=ZoP7iD&c1}lO~Q9WZiZWM32EKY+%p!tV=gCJ*1GE^FS{p_ zZZ3yYE|Z2AC)bj?7Xb{$vdc;N-7Ra`H>Z~q6T|p5$2`2e_gpiTZ&?a1vESqN(I|fP z;8py#INWlXKbh1$+%lPRxi;B#+2W@F%#9mfOS;Kdi5l^{Ku_RFxD27fp3!0z9cdKtM&(3wP zncl}YI^Mu1?-S^(1Om}`@a72XRGmI}82#u(U_spIGSi#xput0hpZi-w5zu~+CJy^= z3nMYw*gL1v z@MX*JX?F<8o?X$IJnfG(l*rE-J10)(e__X$c}V4&&ab6?7E=Y#uh5@~O`C51u$Hxz zR_^sGi`*3zV~p5i49X4YQbu+`r@&SY;&qT4!!L;-Lsc@Dwnkk%T&p3?#KHNn8K<)0 zAvJKR%&MXz>Z&k>J{qk3wbP$2GY;x-)!k=kjXYx7L%~%Gu6`t+7b5<^ zF|oj=_P{Zr;%0V?M$Pxtq_G82OQHzf9#54l%z9KhIdXeb0>mH4A9M8^lYg$x1CxLG zZHCr~H>T`TUIdLP%oH7G(;wyQXns1x9siB6-g9v-wPR?=C>39caHcv4+ZE+$X)yv! zS!+|Z129Q~|A|1W^C6aiXi3TFE42iPc%Ud*J&;LE0Wn&`9ti03P=iz)&*c6<2`>GyX*ahJR2ui2T=?F(WsZ=(mGnld{0yj*JVvOMS-v zkAofFS;(<26uCZMSj~PE+u-m_y!1u-s)l^12qYR=r<>#?h58KxxrhJIn`h}s3g}s7 zo%b%GaWuhP_v$RzcrmI{GD=D)v}W|Ra*3t5>iMeZ(abe;4c&>2xSDIK0A;3Vx*EE~ z!X&jMRa9l>GP?T0#JVJn303d_QwH4!%f!kg^;%WT0p=#UkMP8nB+X-0L}sS&!fO1) zd>yqIRTO6CqQbhA#9AGVK~+#0Q)*$YUShe9dbuh_8FPJM!(`$o9nEc3pdFL+gc6A= z9!gsAgj%L5njQ1I!uqcS#HtDPPpVj3%&mnVPZQfFG=B~uJYov9 ztVU1F<57zkLVmT=?(mYU*h{?^|m3bE9Q3i$D73VV@-MeykfnX zpolRTiHfWPF-FBVz7~bCC=z8_2mJW*alTKzW3Ne6EgZ<=D~|Zu7{HjAdu4x;s#Oskq{6CmahgR6=vWVK48g zYfKwUGE`A@c*I_@Ue{tU7GtQ)>Ok!dsW|ay;}{G7q(roX zYg|6$(Nr~-`bp*S4vBHajz_EWSo|kt!5zY-@&%9P{jr=+sxNmamMX42+9}3DeUz|v zaKy{|P8)N_l6_P(cOHpXY@W85jK%sWbM6qdmd~7iS{}>tQFYlNZ>_jEZIc*_{Ha8_ zgXdp9dfL=Bmi|-4bcfWx;^4G3U@YOMvg8iYZTZS+^WE5+pQ?d7l(!Z4R9Xn&E*vT3 zFgd0$1>!Jyp)f@xIXOBx1$;SqJ~>77ZaKDY1+s2=v2H~Gog59F0xq3A51k@vp&U!0 z0!g8~NTDLwQjWn=fzVQ3z)}$tF2?~^pn%Iuz!eek<*4x$aPZ~1@fA^0f`8N#sTc(sPD$XPUDz+ zh-8$ipATEGpq;KuyUjflsM zlFSX#<;M8R4dmrU`@jux=f*mzK|rfPj;%pbtwA5E0kGDfR@8tUYA|l+L(iKV36}`_D!03uv;tIBL#oTm7ByvSbcLhCm#h7sg z3c8|wbcMWh#kx2`z&b)sI6~4qLLWT>a2}z)KLWcPVICYIQXZkaIRcp;VXPbhC6CbB zk060ZqurBGYrc;`;ngt0K^Y@rG>30&E%Mb-gab52__6lmY@d3nUlR_Q8_0 zSBItb;~L?r+7GZb6;-FC4eA(?s5)%1wOUojr48^H5f0hUu{Ez(XQvIh8Bq*5T(Pwi zREHS!qZ;9`+V@m8rdB5z45}DCVs%)rY|*cdF&JPmBB-#Rs{AxnooO&+Z$w_=^(Y;~%RZx|3*AY8CtaBSYI&S@C(T%cHR zxOQwOuMYL>$6UY>weQ<)%&AWH9Mo8NBp8%&K+tSIv-@eOI?Hp&d4asy z;bONOthQS(_ z!HS2$2DR9drP!LJ*h-|>25fD~U~NrkZ6#oBgSlbJv0+WIVI{F)gGgveO=yinXvIxv zgOX;+oM!zf%}O}U24rAKZ(vPeV8w4>gE4K%K5b1tZ6!W!0~D~tCavMl;(Dz<7x!f@ zgncgN-CTsz+^fC0VDh=>oVjr0x%j2IQ1Q9g*11UkxrAHcAfRwmsBoC9aNK9%5L)4w z0^tY?;aA^)D*wa6e`*j+uRiC+mvwa9R%@?iu4Xs^p5NE4q^0;De;c5@qV@G z9ZcjMo$ej}*gJm4J5;?OYek>t01hasD!I9&8xW4s}RnsnDCdFv(<4)%OwS&U#DBX^WCpS^DtUsfbkmt z&GuKnPosw!5<@oEWN&s({MtAV!`u3au5nGbhy0q}AEveqKE5U~-P!SLbvcZ08xXuE zT-jdmYd$#4X&ZWZO|i0b?bl9u7#h%zb&Vsr-FMUY<}f*6Q1kkcsd!^pdS%4@v9?a`a2_QUkMLDOr}z@3Ae*1*Gry8+2- zqWkTYo96pN)4PTHOVfKISP&Sy8UaXwj3k331C{~FK)R5+z+Ip&2n`YqmtOG(Zhy6=AG?+g$8!`;NMI1_($rRzUWF8n#p{5`qw7jfplom{9uJ|yn_JlRKz zKv2yi70e&Drfm*g4da^gx&tDrKG+-9&u(|?JJuBEk8?%UPZc(X1d?>5jWqiKU$CT{X!`@?ra`weQT{= zpJlV+G`*N%vH9YWs0-V1`xIOIUSi=%>Ca5oL`lQk_a?X%bQWLUha!?;2O;$b5x&Lv zf$%AaLJOS<^9PU~0r-|q3$_2AXb8v6;uv-*+7rN8&`&9cx1cdxTM!?B1n3lGE#>}}b&TQ% zf(K9pkQ2lqW%E`P3aSImBl-siOG&a+%F<1@A07wpc)L{ul<-&W7I*a%<_^1O3N)5#t1`;BC2%77lfnstI zJwYFaq#-y2r*^PF$*akpAXXx#0fvIkf`HW|vj~ph1_aTd_zngrV>Q7nco)zBY(`Xs zQdUzQB4i^Hb>KY*Eh4;xVpS6z0x%IYf+VpYYIc`{u&@D0fXHC24x&R~NN{Ke^(rwo zq8y+`3LhJz8$mIsK#B|-90oj+0>hB#fPKMyROI~togh6bJS7C&V8>PT=Y);O*}xGX zQ4rZG(sL9cP&SwY*wmrCO7Wbs5pfZz3gO|r)bU~!=Q&yCa0aD!;H@$rqI*?&7x9AA?nUk~ZxFtMze53R_x$&Gm!P!! zpZBPKKvqyO!~lI5yAtU&;uB>qaxUZQ320B}Y3E>&KA-~K08|NhB42$BWq>L`tvUyT zABF0JctO0t3a|lDCHRSYwGWgK`l4eybP7;`VU1!yK1^1L_=L538XDh862cE~K$`|s z(mx@sW`L@9&Ij`Y9WbZKD_Nfia~4-6rgYvnnkXThFWy?2CD-|Q3OyO0kc}wX3%r!hYst| zFTgCI0Fk4}EPC|-RJWrzL>-ugC4e`K>4-S%R!soKgvNHJ1-(NwLTNyCWS`Zkwt`l4 z5{9q?7QhWCj=Zy;)fiCH&W4b8KqIUMpd;F>1{WijO?4~uy2CSAIAjmJ0Ng;?0B>M! zAU06$BD^MgjrN*#){HB#8lhUMIush&5z^t;dD9Wn+0yCPA=B|HSUy-jv?quhga(WS z#UjKap+V3`i8?ugjlrTA zp5R@U*{9X+51L;Z!B(N`K{*IRpk|0CaF<~gSZxIT*hvwxgxHMcNx4fh`=VMAdeC7K z+zR%^ItT31&+=B^q+eK6i&Zm1H9IAOS`mHG&T-i8&u++f@n&tRIiaSVC#EOu$17*` z2GUmzc``jJsp=Ja@2m}$S`@w**0XO+jy;S1Teg_0& z*4B`VS^lX0Myv3OMBI9}@d%GW!?0=4>@2f>XWC`9(e4D1(T3=;;7csc_o0z3{(iSN zKmZm56d6V#oSBrFmYe7;#wxlgw?E}DQI(|9>tbPf_CCySk$&&fqRw-<{X1S|GISeC~rWnYU& zU)u>foM$(kpKNeGX|z2`y6LO4+G*Gm_1t$`Y_&OPyM=Uh(b5#;TfFM&?O~*2pf4%R z753e~36D+ZFlD|erI7H+zJPCi*yJ>6^SRyF6J3`+1~SOMVHyhbeHJJgQ#6$Jp6Q0~ z_6m0QU09c5S#orKK(_=|9n+{2rX-8UD#Z&q=Ifa#s8 zK|~NvP!(1cs0zawp$cMx-a^-i*~sDL{T%ObaFw8gHRLgX6X=ZOjB0`@cK zVM5vh_EJAIT-EQW2r&wJ3=xF92by3kqPK8(ksdaz?sXake+0WgmeE^;y(|tHR@FP% zJ9mO-AT9{Y*ey6-kVC>%_Ri*zm%sy%GiaH-h0#m#P-C^V(>KT{^aAk!eHr9ObxC#D zxq90P30erb03ARikQTvyOqX1Tqcesq^ihbXI`I~9X80dFuQ$o;^V zY=;A@x*eNAqevftZ_p$N{74$bF9C<)tE`>rL7X5{P#e0R@FnV@_v%R}c?VH2CwK+W zhU15KX?-}g>d^5qUie-# zm)wU>4(Ses4!sVM4xb+)A6g#T9ugmZUwyhdw<^5K*x}x(8B!8d5>gUe5^57<6Jm-a z2??OM0bDX4eqBxP7!7&@Y6k|8-+(Wj4u@7BcN~PQAhm-6ux?l``46vG6FMkE+W~>7 zcj!0#m+pr|tDK#bL6S&;z+A{3(hc6F&Ed`CGr_}yRm#qlkapl`An6St_de|scq4uZ zL4Fh>Yb&dinfHVXnH?E1515Chjhl}z_b6%&`zN*)H9ED5Y)U8-q8Wl2QXWJbJ)b9x zS&jhNBI+3XLDfh`FN76APBzL1yPlejYFlk;__aU9Y`lFul| z8Kr@pL;X|6BeXh%7}X4oo7fy#PBQ8a`!f|-dR7WOk{E#-+Z?IeHi{ctm#SA*E$B5W zH;y@Ow`Ei!b^%qfbX+(wgd5A8u-g&YH);!;muf?HIQTVM4T3pC7}lnmKJZLlimoD{KyI z4eV>`XVlr$=Q6vYLI^da=2&5*VVq%%VMJk)-O^FA*ofH6*b>+Q*!$QN*umK2*oxF$ zROVD&)HGDDq~&Dfq{C#$f`!m=kZ{m(061ug;6#K(f)X)Yan^9V*`q43XQ-TIpG7tzy5g;gb%UZrvD2xm zq{JfnL%qPRlxv9HXi<#Vn$#vTVqsoLM`UXp-QrOM ziA=pV!rQ?rCGkmj_21 zf$D^T5@z@4W`Sa6fe8eGl6+@F7n@(tHp2>xw?`w>?!EYAgXyFvBD$$um9AW1l2idQ zgMo@SMcNpJ6EGDio*+wPWh@viBN$ERvd;}lvgpd** zm3kN-%7mrT2Ws_DKJtlYN}e!l>Dov>;DnlzJC%AcAHjs1QY;mFL>*uPpoB)v9?FL` z;itq(%^qBbKf$7uMdcJx2bv&Ja-en!twWqpP+FjV7g|R#VW5NwGmxD}v!kSARBEOY zmT?QNqntogs)BjS&WGOIYZQ^gJY__X21t`A>9CY?VF0QtspD{bq^X{9^ZB2|;D z2r4tW4qYKsi5iwq9WEOagb!rKpTJSlhGEL0hNK`cqZ4noG zDA~a@q(m^7iFCk)YD$dMR5=UQlG$EJcX>VDj5;9EwaKL7>CR!q%Cma z35-MP6Ur7v;Wf-z`V+<$W??fdSK0?@i@UG{Mj_>cyd_*Hrld*L8gz;|p{n$rs`Wwg zY69}GDgxe9`-hxr7X&JsQu~Ky|B5ZQAeM;C_eFPTZg;WwyoH=F`u-j&qR3~swGwb# z7zWi4tez@8q!R5ZRxP!qQ@;p|RZ1Y_8KE0}|2h?^Y!!Nw*n{3C0d;!BGjJ_4oCQ`P zQw5$wcYD!)Ml}=Rh&YE^D-0)r?Z{REn>gTLn1f6esEHYF4KtE)MsGra6T(PkoROLc z;2SU_S!ZAqFPsDRj_PCNBBC2szcKa8P)Cd=TDS(RS=Je?i5xBs^OfBP&Vk(m`+-zg zVT*Wfoc#^3YngqBHy2zRrYK7R@g|0UgE2}`0KM7Z(J)0R3KVZKI5jL%CKuBi7d`;9 zk;+B&W`NtlN~Cf@bC}0~{vj&P5HYM{zkW38(Fiffv3b8c^@6Mkz?&KV0`^$O1mq30 zg!ETXyM(oXuUs~cefo!}-v_mz9DDYQQXPcvB6|zM#bJW7%K&dmcn<8P^fHz=E8Gbt zPAw|y8SIB$D{6_=A4%0L;~BaOx|ciYQ$b|T!*?N9T$X|Tov=_Uh!kcx89*4_ zO$aX3&kakEeicau5+-qDgVVwiWHf@v5QTA%UHb=Mx>PpO8j;zE!gy{paJv3Em@ajR z^hmHD=vcl#o$5`5ANUxhzX_%(BLTYNfq#IFO1Gh2iNVoe@1@(Yt{C7(FiM#=v?~g@ zBFsg$4S0nJ-+{SE1)yIE!--%gRFc6r_{Y5clvM2zH(l2fo`F(=$rs-z1q)pXn+YUYw{8 zZO7e?>m9f1&AaRy?(G|P^-@!QS_|9t_J!N9?EAJ$a<{t$xK00+OS~y{V5kAJEkUS( zt8Hpn8b{sfTPtLCf7{d`13TMCQWbFx#~~B49HIjnHD7npMTbUePIdv!{TwwzyQs~B zRW&=i;N}76nuT3VBYNp{0>AH9D}0hMA(R=Hn?zG9tTz^krc5{A+wTVg?`iLE4Dv=V zXBh8KR@g4TNJ{jsV4bx~-n2_(23`$q+=t!X$$~T|+HY%QC>wqN3lfg{DAQ3TKiz+1 zJbrJAbALzL9w@Ctp_Eus`om)I-Xdv3|Ah5Eyjpr^B{wbbF6sUn?dszF&DlWDifKxF z8zHEjImWN?K7D3{QWEF>r)bMq-~GvZol}>?hxfaqfm^2cEcbqPci%Iv(ns&wbH@U& zyzb9}?{}<@-fO-XpJBVdRpBcBpc1)k5%ZN)P1)y~yYOWu&wa8B32r5uD&Gel#KCeo z#xMG)b5UcKy!kLE6d_N#{@G9ja~GO8fo|+bTtPBrvgb5Mon>+eo7mXdnn5nY!IBUf zpDdJ3Cq|8r2~;_}oz0Hp3&IaL{&1k>=L@x_=XMO zZ=A(Q&5Ev>n?`%z(joOmTW;a>vgECmqauA!ikM+{B8^r)HjJoSak;erJERj+VZI&{Wu`|tmP2;J!#k<6 z0(Sf?r*Lbf%Z+y;*(i~4DG~pB6&>>hRcfPTS)TwMAb~E`i_NZGR~a=|Ic6V_MW!XD>BR>NedP|&1|Q4*qJGFT|Lu5+vK>R zQb5wGm~!TcfqX*&U8j6fT$@vKTsKY)V$PN^3H@6D0GACS%baVYzhJr}V8* zs%XGxyL-x_{d-;8lQ+^1_hN>-MB_XMQfN0K^UH$c6m7B7$E|fm&EqWl@>xw-PLrK= zP|70zYhv~(lsavjvGQ#7=F_M1k(?7? zx_0lgR2`N9hO-6}G#?`b>fY5g?G@JVA+TFl)umod^b$x_=n?N}YuDM*Pq3t__@>lS zG|JDnqH~9*R z`UuUs)C93dZB=R?v4^HwA9Noh-ys`*8!UH0kWVRFqJ)a`^E*pYdu7*)@U|7&#au*@8#PguNaa|TI#%bCX0Fe6;MF&WD|o)dnP3cq63dLBM_C4e`( zAWwG?G`NW$^fD?cC*+4r)By9UI<-5B2N%4QZ7)P+#2Jr8*XuQ4*o3gU3czwM3o=d` ztio^9kFZS(d0$@IqVi2A4XKI*@0Cu!t|$Q;GrTMvH8}!XTn9U`3&v3Kq(4^(*5&me zgrZ<2wxC~H(2)g{dxK1x{Wks6C3dzl$V3ujsFdG1*ZByrXGm`Q%84QWJBWnLk+3R9M17 zvaOG+PVn@UIS0c<`I^q|gVc4H!Y-Q@ABJ9{jy5+d%&|-@x2V5?nd`@ zLSpJMzJ|LK$I}psmE6xUNf?v@IX5S-+j+q21o`zGL4 zd^Drx-cysdDjV{=Q+cJtggc2gHNo~$dU4S)W;N`O@Cd%s5vs z^&2$ikcElbZ7!;xOZSkJJ^_mlu$2-c5}3YSBN9{7=!8yzN*jR*iJV*-{5Q;Z?RvEc z*5e^QAuawf1DdXgEQB4bmUp^w{xKB?h{87%WdagCQze(@%ZaTAz(2W}dm6)9!E;rEADgkym+dae`1PjzV$83YdO+*4 z3QRrAg^fAKANRG+UNhOvlxG=dNhLN}rH0Y+eAS_2jYlnF|2i7>leIliwPbZhxiSfD zl2!Y#K<`p>T+{N5QC@Q5z;vAbwCsn);f<<69oJHzL>v?LG-_a+BxT1NN&}W-JoYn} zi_w)6Q;Jjtb}Jc)qcdfB9Nj(89v=@rI`>()@JC@ZQhrk}`1Zp0JXu zIDRY)^U1m`dqFXyN`+`X4tX_U9ouvv?PFobit?>!7JH9;lAKlyTXZ|6$meJxUZiEB zR=grH3u85luSdRBZEG*8F9|J7)HuqAs)-s(T~A=nCtz;gc`#h)eZ`x8_-`We5sM#h z4JPmewv$9sUhTVj#4SuTh{V6zf3&?No=iBWT-QE7uHc}lc)@09xjAOm7KM3!Y;x5=g%0CT%337O^>#26q~`=B+Zy}3tx z_+$^HR|8sfW5+qlCo-OSO{+BTHiYY9-{fGCI$cbS;Ji;9SX z@bN5a1MxpTJR9r$YUYid~zz;~58`<{IX*q$Ea$6WJ5ly~+E9Qgw`bdA#bZ z;mLs7KTLr1h2*210Kn|&2Nf%jEu@3~fAHb37b6JktwVMt}Muq~dt2Bdl6xTQfu5;bu&#R2?wdJ|@O9J=T zu$u$frmt_Eh6g;k-ANpKvwH>2uKGYPMFLHQjQtt2m*WUWD)HT?oBDIE1=dCic*0H! z*}J^M+nz5I(q><9z(=k%(Kg>+bO}os3YB2ljLcB-Y}&XtskWzgSq)0<*Y@i)R{D)R zYe<7~Tr+YJt&|MYe;|L^CV485dT8&-g6%0I$kgGR(ok<=eaui97l8jHXu$ zBgQYEx;`%X)^!$D@mzhs|JjFfo|(t`jYiS&i|=a7p7RFMEzWyFom(Y%K1vfcE@Vo5 z+qg+?2IUeun>mFUHMBo(o|kQ0dPlt|;exj>V$ij;`r-L|Vd+M}_sok6OYr^azKrH~ z{O)fqGn?K!bSzzD6YW<$4)YDGdagNyz0YWXa&gTccD<0(b-N?Buf0uuQu^tYZzWl$ zxBwg(>q6@r4v&hL+!Qti<6d~*7nZK?omk(F{CQ5;&Xc^;JS}o_o`r9YaW1;g^6U%q z%X^K?v55&j;vEHEWRz?=Zdj#s{ePUDRZtvJ8>asl5-dP~1b2dk;10pvWpIMS3T7Se=URwVjIHt(=NG-#8VI+Bg;8 zy>zmb7j&{!enkDD=*hX{=*>C2>!n%cyH~e-DGd2H*ha>`u@Vk?dz=xq46m&{xyqtx zi$C~-+H>x@yQN%vm{Rn7@}~FQ6zwXC6GWAW>R->O5Q(X{V`iCbciDJWoqU8TU$fnO z_T_UV6pNz!t@hVx&Bea`L?NZ$x6+a;s|`8QXk>Z>GLXDZUBXGDuxJwh7C-B7x3bvQ;gQ(+PMw;Xki+Y+IM13*`xs-Jg}nH=|wa7-A{ zanovp^RnX|XuA=Ldp+@wrnZ5W(QbF!3At!McF#sSw@{k zVs?36JC4#qYpv3_F_5f*GTBlTq`mm#U`Jw|K)vo-4uPiK^LG?_T#zBLLArOW^0F2p z+k4u&;I)=eNhjnH$XCD-I`GOihg9=Z!jzg$qFgC_<~wh4uI(tDZAGd|?m3$qELDmu zi9k6ew@|ZiGO=FMxK1~gucCM=-1gWxTg9FZtbeXhx}pr9XEbP;EA52v(wji@DD0zK zf(~`I?5zlezk-sn1{a68wxy6M1j6UWVj#IdhbmVgY%<9~I2P>#`EaSM&T( zh+=5oH^nBF5IHSLh7?U^d=_Dq9PO~UkDAG)aq0?8+1?tn6CCf%Mb%0mwe%%6gtqNc zUAN${qUHQ=Lpr?4&N3AXWe)tV+1%el$MSFK20O5=w6K{IG$mCEc4Hxq`GbWg9>6cX$*&*6 zAnGNu*8t2y2teoYt@ID121^0%8?6zYNf;x8HmN#GK~Pu-{8#3a|H7FegmK zQA?1l_q8*}nX8mQR9V<}Wltd;Zx*Rlz7%kWrZqs+_vEO}-fd|I;k)RrGe6AQ4}I+# z)B2Rg6}!$%r?c=S(V1oXc`baMIWOevf|k4$Zm*Vg=_%wRkgqpN{?b4bkc8i4qOu5K zU@6usu-2L*tXxp0Nyw0HNU$G5OE9yiZ90^Oz*E+lTXpiQqkl1nV>){h%nHb#LZo{B zz6$a3#NR*3u@6JfiL%XI=Y*()=6epA4Hdq|$HL4jR%FYGeVt3K08vq3^vqOoa`KBxfR~C;Hbg3CDf}mR`zL&YfJk`op0-9HR`TM`wXKC zMokUv(K;e<@KU8AV#qvGRVi+xy*SJK`oO<_r6{t++pG1uS4&TVK;r(>UvAROajZi2 z%mC7muW59&-}@|xo7jH6sm$nq*$oK$o1P|evP6mR39Z2JU5tF=FF1yaY&q>lGM#t( ztcG+u0D9I2GVj0N%D4;gCsX)WB~d&kQvXu)*p!&{m5%vS|K74%=e3*2;ICT_GYP4? zs_e*1nV4V>ytjv}zj7m`CrZFIpo@_BtuLq`d|N;A=W0Vw7-7{GK6Wm25!^eV0Z_`<#!2&o!>` zyS#!`vVjZV+4eIY=DY<>wVLNZya@i#y_YRBJ0*~OL=s+9J24p%ymS*j@y0@Cx8aU< z1}unmnetcCWxi%&XVh!Ak#{|$idas0ER2qLv^Q3kyEm3HyeF0>t0$Jz>tG?zE|C<+QoR>9qNWPMF2Ty)J@Vn6xUHN@Zm%I@69V%7poI zREGg=)FT(MnSq*&-qs}T$XOBw$UE-OrNela?D97=Lrt%RBTe)@Vq z5iFmYz+hzh&RQQ_#xnsy+a_a&?@({_8&yxV7T+a({mY%rSH_RH1I3DkvK#H1W;#8= zx|F`YJg2=!VB>eHgP@L?bpOk+)*-Z$-}g)xLdKa$gqLAVQEV>ZhXNiu4rxl0Ui1>u*0b+olg_0#4=ZTTv_SX;M0B}|=9s?sXSbh*~9 z+Zy5J(levW6kbXr-+a(e{@vx=Ug=-;>r1>)7`sB{fHHL%4z2p(R+HbJ=lu_7L4QsoLY5W zBjs6a)-amrLC;|e)y#LCcxmCuC@0$O);s~-K5cUDaFYIgWt)B>zi<6$i}yUU#oP^P*L& z>Z?gwcO?KNGM&L3kMdo0HM_pOJ88IRtN7EsG5z{bJBbso$7$1T9H60) ze=abJNMp_mv75g`($zW5%WpTkN+hdhA6b7>b*b&alAD!ru+kBcL8rXx6frsXj(y;uGq6hW>uxoM*5EEycC zE~jB2KZw~&kgrNFu2mYw96$S8o%|GO#3h)K|98TPmmZ=ZEgX)mHje=|Wq6t#T?F%uq& z7!TC^3Dn+?kpz;kx4nTEyqGj9-{cu1DwQ;HG%edT2_y}qXIa%%i^&;&M?8OwrzHF6 zOCB=5$15$(HRN`-nx&(4RK-~NrEG9HSXIyy2rNF$?gL5|({SE0TCM>LtOH5$F^-gH zR>GVmjhSg?G*mF^8B$j>oF&_z*Miec_c^NlE5d8imuLr^OKp<$4p*e)ZCgG*jB2fNQsH7xd;K4n0O0rq{WIS$m?R{@xWhrKki=|JK+G%z$1V>X+ za-Ny*;zzDz^dN;(oXCI12hUG_txHgSqPa+vlpG&;d77CknZ;v*5&7m<{HcMuh`gLd zq`ESG>W2Xj+riCG*OHwvj-|wj0i;z;4%KX9?tIb0u*`l0lx010h4|E0Fq(hH>~sry zdd+$1bzja+>z@-CvN|cz(~2JAx!O)EwiX^D-Ynl^n%=Vx(w>P6I(nR$3$vO5SnoBWqx@7v^pbA?;iRtwB0(q2lE%hQfpODLATy| zXQ&8g6;xhXkeOgy)iM+yTt$CjrBGNYGRPCnH0#I|m+C>R;5QeNuc0G7)q_WCcVih_ zr=moE82Vm8!dOOgnJyMW%n?_~|4u=zTYt(>Ez!tbMCmgunxWBiJihgqWbnZ{WOL9% z`mC#f&C3cakvyh5j$6d*tha^s##%7;-oI8pmQ7Ew`i`xjT1@HVm)}JZ51%v(WkcTw zU(s6ayh(JXh7{0SRYBm4B3lJn@;6P8qO(I`61qItY*S&~(oUq>n^Qeo|wId{-34DyaJT7+MeJRURi z6y0(7MSP(2Z6u2JL4r{&Ku;<^z>%3N%$Vq7!GpAH1`AuB4)*Zw<^b3B2=PaM`L^(6LTt{GB$uV3#UQjV4+t&0+)1)b=}@(O8KvfF>Enad*9 zhd)RsLOTY+BDVE0$@Fx)2oo>={b&4h`)?b-E?o^71<7JYLx?@dB35(`U^%`=Uqg~) zwkXVBWv|AaNoNz5u88BQiU83TDNv8^>BMefM{fv5olC3!70o1ENZ%nhWOCL_JxOO> z)mL4?_0L@<@b9Xt7nnR~5|{*&i%%Ab3Uk651=j3F)jat(n0{G0(SlP1oH}U5oel+u zt+Mowb~i*kYHA0J^^Ge+C#4_gAmv_~SDH{lOu<__t=g9ii$zm(ghDu*U# zk*7b&`_g2?{3ZYJtKkY}P0mJ>dbHzD&#J|XB}`nBeEw;Z^s&r*OsSrTRkwtCmf$&Y zEIcq=dxS!(qM!4*t95NW=QaQ4wAZrn+LX6|VWWn9-46A9tD`ue;d8?ye0 zKFZlzoB~feoG1l1fFGlFY0L!7VEZjBbfwA&=_2nDPDfPOV?hsg`}_)*L|N{e$|!1ul?l~FuzPUi~AJixoc|d zLiNa&diN+dnO-P*kd-TFbrBeEcarl@YLZc=Qe-QvTBB00LaSC4A?k)QhzGrxe@SWL zGlSHCQauD%3IpcY5$4)S``z`yjvqSArbY)}G2x}RIvqiqVSMcf5vTK>_C^%W0{>r- z?xP#PrPoMo4ygydX#Xf=)0ZiDtl5CPd`zKJjXvP1nQmh~uN-NuDcvkpF+8%T_|S4< z6zF2I)8??!G5e(Z#MGLlsKzj_SpME{e2!#%&ar59%ro}XWn<@(im%G<+q6)Pib|P; z?%uS6{qZ4B=U&USTUL!2(s!=6l?gJipGx$=JXn!h#s3lH+_@3X%tp-%o80wX&z_XwTa#RUIWWuBUq9+Wkl>lHc$6$7K6*e`+aD#5dxuC^ z5g-Mu0=cQYxH*Omr9MEDR&_w{V`MPj7%+Xd!^j7K9LXF#EIcNyWAy*rKTa^%>o5$H1fEMgFfOSVh0hB%`QxPtuG{d=7&Yh2&veeo}MheqJd3`qW_*;l#dSV68NgnIZ~GnD^F0nju8Wu`96Au?JzQEb}uFcE%jG0U2e@ zxGCpjB0>GT;>?`6Js9Udw^v7D{Py!-a?PaE@W4fl87qk9;K%HCz81a~3+(=K%35#l z`v^2{(sJhZKDS8Z6NI{3v!J#{f<{0oc>Ao zvGcKW4NBW56CrHzeS0!R4scn?3!A$)*?vew8Tzwdx*_J>#N0NS45aFV63@QO@XtVv z+pdO|Skuz&oD1zpw)X$cR@?X21j2ArI4%3GPcSK%T+{2n&mr}p^QJ2kU@89o9|#?H zfK#Khnul7nF9k0b&~gZ>9UPybnt960`mo~q!!F1!sP|Pkx*OUjK#@YmXZMzcBE75x zKV967Zt@_(5q-{`#dMotG7gmK5sVWUzs0CD{x;JP>fy!q#P;OUKThGVrA^ zh&STMHKshl|$-v@CikH9riaIL0vq&(9CaYirLA&MJ3| z15B?T3Q=kP2o(QVmMvNmSxP{L=4b9j`=5)yyt9z-`Nm_u8hEG!6WR>LKY#y!hoow9 zBBGZU+CRj6M$k_sZZU5$8Q6FMUS($*TX~u20hDB)LjQwLtpi~p=p!P1Z}`nQVdh`t zV;1l4w`)4N0s(ICbXhc|Y)h@(-gA_j=RtLf11BU$PiEMsBo>awaV7s8ve0A&R8VSv z$Td$t)XxvBYD~qtE&DOiAdkNGj#q%E@>xCW1HWs=;&$o(5yIFhW~RHNrTEQ?)f@S^ zPwlFHE}s_e`p!Y8RssI3M>JJ;0Q5Eg`oNQz{iaI|I^`7^A&i6{S|RHl3|+{g8U#!T zgAj`$`_}rronjU{SyObORUPf%d1nVuUb%#lT+-en z#o()e@@PclC(W+{1?rv$&z;)=TShwMU#$7^+BH-?>yl(FH+sS$iv($bpL%Ek7yw82 zC>^Y$kXb^{z&K+O@qj6MqDt)nm{orW;Fo}DkiX1Zj90UshCDwJc`SnuWk0@LJr9`Y zWh*Aj_(PfBNfT+X@TJXN_DlDKD~ST{6Wtu6Wz)kzkC}hznxfarU@G4n>zzyzJ1Xrs zH`A~>Jbx`Z_W&I{AoG{TZwA=U^C@^N9{>yLW>S$JyiD`y%eFpwrjh>o{8d-aOS3r~ ze9%+9)1PU20X&6Zgy*j*EGjG}nRD=WEMJ@4KJal=m}K6ZZ6P7u4Ax+Mp^3@9)0Pdl zMvkNSWu`ZlZ}_n66E%gm{-XFKbm|FR4fgBG)2)_9>Qep~QUqGhxiZ|lT#oHedv6qp z>Rgd8xcoagqNeQ0latO;m{u`NZa7(50#W@c`%s|W{>>U}jp%BT(G4#jTWuNH=PGlw zhb2B3sIG{}fWqdMnXb4GZ7RPT>RVf-_|;ztX7RS4o+AcP?5mCU^^fzxrc{z6;q9L& z^+jryaB5Ag+xJI{QB5lwy2KUBhRrCCkJ;j112+H0F4EN&kt(|-?^gkDN|VY=9KcPp zE@eB9{OEn;9|(n*FV-u<)kMWo{YLJRNeonBl^tarWlknHW(aT*B2&!rHs*~OmI`rd zIc4kZ^1h2|W5(Uh>+4h_o*UPDm!k2ED7}!i^?U)l2{<)^y1lP9wHt)%sGT#}CrbwJ z0P(jVY|{q$=ItvZc_~_b`npqIb$igS$hXDLsJU!4*;W#^47ovP+I3L6?I8jB$HQS9 z&?wT^mNK71jpAAbQ7%V2tYGMGBh2Qfwa$F>_zsmD_dR$)RoIgCl20A#g9!(s$dxKQ zllYYQbacXAxBs^)4u9opMs3kXW)!8NND7m%$o|RTF7c#9iJ9HDBiU2adk1UnDcknR zTb30akamTJM`J-pf&Wgeae1pAiUN+ZUQO#p;(AaXiSYNVxc2T6?F6c&@3}BOD&Jpv z@H4I?UtjC5B)4%s&mW&%Z{cew9Gh{bne$m8hCV{LivhVSVcx6;+x4U?v+af{6}2e2 z!)PIOEf^lH?4P!G0)7Elgz%po%J>C%Kn0J4^*cAS7=nCJv+C|Vc$Q>llRO9sXrJM0@ zEJc9A_c@$RH#-=8z~>Y`9*UnIjN)9{Oe1`qwtG`JmQ0C#_1}S6tcFLowuuY@*ySJe z&cPLn_164iGt#h7f3@jMVV|QqQSK&W+zk@+8awnPjEzEHXpvEnUGUI(H=z$ey6VyN z^tn*eF2j0>0_kyau)An@)Lw<^!c<~vOFwaT55H&q=KiCeR>1$~L4gpGg7^E5mmpO;Tz~ z*Ghx#l#V?Xj=!G_=HSyz<|O7MrdV;!`zKTN>$B^ISJhnCCTbQ@HyTbT*y$A~)}csKL-JkhtlE*}85|(i5=IJ`e3T*1V=W>y+}MR3sj} zcofnAjL|A7ATRF=d)m*}uG;><3leFO7c7`k7Q7pG9%=?+d#T1c4{ZTez4V_o0r<^J z=~=-*PcKlcvm(RNL4KlEOY!qvkai2AxVfmtUWBdQJ@zOw#GLTSRbqu zuSi>SiWw+wRhk+%rgO(UJ>x$_$l@@jE^%(U1p#0sXqf;4XZgb+T4D@CM#U+b5{YlN zAmg*|-Pf##8lMj*P7KikN-pYP2nFcqpPN2F;+E9x)bq1yAgSAx|JAtEgG#;ptSRkC zf5S&PK`TFA%trvXRrqsY6*I-x$JUCh_31Uiq-4h?`RUFvsjO6b`RP--ILS{Ah2%wv zTB_>Z+Z)gQs{*EXr^6sxOC?(ntWYu5T`!XtPM)quEFFw(ChpDJ7e6D?xRLyC;?V zxv_N_=51F!BneZ9wW;|#`<9MmpW;Nvb3v*;hdZ-^r`SgZn`?=0BCh^m(@=Xb~ zAJl9V=7M@N3VAbap007K-+N391G(3>xVa&3eaLnj9442<#TbVlud3kvj#L5Grz3|c za3J39y8UIHnl^j*%$!Xa!vi>ru5EHw^bV=*E%zy5N5&V$ocA6YXu->tT)CT?%C zkXI$Qh;l~T!S2}gD;YtAViv%U$;x>@PpZNlejT;-^1y$l)}V~e8tHV3R%Gk7Qpu6-W)Aw zt!+JA>F{Zl8~)4wlj@bYtiIecuse!priJWjaxiQY)S!iP+86j^Oxo?rz#5# ziwv`HMYV2}xB3&Pjy=;(Dotvf>TiuR-6&?=w8a}A(H?MQvB{i(J(?#T zzGGSK=;dGTqDg&<1N1)(}O8QNUW;Yc9*dYOouy9ZsvaekT&%svLL}tR~b)_saB_(-{j9aottNctFepKFKbx1 zSh&;&RlJRyYW!-BC)I14LGkd-2JmddY<6Pc zOY}EU{=(MF$xhROHrlBgb}FO1s-X0S)uqV*xquy&G1ClKP-s=NnG0bY0a?eV6ypxd z;kRZ)Wjf`$wMwJEQ!x+S8PDGkyuB+S(8?CRQ0pq z&E{r2KO+-MVhOlq?4ovWN5~i{Jrlb7ezn8O%&BJ2BiFE&d8=DKtpZ zkZXtQgI-R7G33bzSB@0zTgldJSYN`erBm++2PwZ^c)xSnBJPPx!#AKTI@?HjT~S0> z?VwcW`66e%LpsEVFp*Lr##3NzEk`;$fe*X#*A0dAADqzFYK&hFB*ybYlJ?h_?{L$cmE#NO%5ORpydtPg$7 zG5ggBC&TY+Rm1zq)yUUYO281dEBrqdn%@IB4ko_ zsDp9bx1`O;)O8y;Cv`=CK@Tx*-I*ppGWPXxXr7>$QhfZmAky-(wPnHAPGFwbB#j{z z4YV^t$uWoTy6QGUi`*H4mQrF$MROLt_7Tn7pbrgbu|l@jpE3yuja9z7a!E{7kiIM( zL|si+8TvAqlr7tUc+Bk7y6PyUMdb)0#gtw7VxBD)U#3U(m`lwx^cP4A@#CLGFpQC^ zS_(x`gWM9F_-Klo%hX@?IQsHu_L*G0fb-kVNqfoO=vlPEKRrM8qV!40J29mNG)m8jsxe-zXW#KINW2mg z2YrH~324(UsVCYVG2sBe!Fc}Z20oMZ(6PEm+(HayW~s5|mD_)VD z^q?-_eX``^pKa|ikzbixmU$~@o7 z4Z^*G>7U^F)2no`k3^Hv^Yk|`dWJ`GiD>Q~e*d^SSnBI*D*8qxQU(*-*cF&kUd?$L zeM{2pz=KD=KC(3?{aE_en>P|tX*XM58au_y(m}<%n@D<-E+_duU-Mu2bkWvC zv`Zly)U#!!zBL;6k?l}Zl7HH^Vsfd))Zn#SN<;7Ezs7|@o8heXl*To`G}A0&rI}T* z70=fQWy*SI|LL1mfavmmJGjk5|Cl64&LQwPWY}6tI;Xdet|BatnrRh zOm0F}W7hzhRqbR@(>}QxYF24noim0G_vm39m&d z1e$c*w@l`h>!LL2SW>iGG<>AuD^x&kyA$U-F{&GfOtzz?9h)mtpQ8fFqhHS{Ek2v^ z*!T(~?aJF#?#%WUhG5F*dcXP8rtgj>{0PyN+qi9pYi70zuIw+;Nwhh@i+dXL?%Spu zly3f5(6|;CHl<4t&;8}*Zo=gi`{fYM%12tN;gPR><=kdAT@KgJKun9v#CqW3P_^NW zgUcN{PBEAB#mtZv$tXu75IKj7{(V<#_r7T<#Vf^YC(aS4bt_=_spPRpEqG`jJtfJp zs&TYs(t^0B+@$LE(4DNPf6T(qC^|}nH1k`s8zcO_Ye_icxX?H3k*!TBWDso|?VM$% ztQywWEsG$tYCz6!T;}%pxO52kHLqFF1}CwY0A+fU4Z?wG4KR3)k6M4HD4Neg@92Jw z<>0M1@&O_8O7=}e7>{yEa_fyE{7&4o{lR(%lxy63;=V;i{E&JYW!Fv^Bs8AVXus1} zK$#}W=hI3_Np;EF;s`upZq=%sZxL@p4V;Z;QoZsQxJh7-x6XI8)VK0S)Enoo5pFxA z`gqGARZRIg;ACy6f~1W3l8b)+{QkjXf&ktJ-f39*ip8joz4~I~^Aip}S+O7=alpgi zyRz)HVn_-0s$0F8m{>?3+1fG4IQJ_hR8Iv19~SoE#ld-G;~@rLJnL70?cfyED6tA( zzTSWl1OLvW#dtM3aZyY*_PG?1ax;*Nr$)F8yZh_rh_kZySFBEQ>+$JPN#E^`5CbGG zrs@7pr&T6{!lJ?p9TXlt4(afJtSVI6K3jL}f&=nMh`U$!Y=W?El%03}URl(J4+lJ^ z#i#Qqn9}X#InVrBrG{Avou-<28-YwWZel--6zER zA$UyMM)uA;f2Y!;`rrq!L5ootGX>zjRiKk_Wq7>mb7CWJZ#$?rt~^}Z=;$q`f0wh_ zADdGUYvAj4I&4i<>QQn$UwH6$X}Y{nhAnyTT_ZS1E2BKegIEl7f4WsvA~%%y!IZYE zr&#tdNDQ0-cC}&C_wDO7~U_S~EYw)IVug6xy4TSM<~qDggBE)7xuW&)dTdu4pFM z=&cuLZobKJjV?R-C0r~Q>etma#k%8A0%P!lT}ed8yr?eVjuLmGA*WQ`go>KMaDU$} z;oGxH*lKmA6k&{AGX9$nrZ(m1vS$STIacrTlnSZDVXEVkk>cUKFUMJ{vm=e@r^4rZ zo-+q`pqT*G7bVv%heIOWn6#}cUZmuvq3Hn^7HD5uG`#Lc!zXj0HAnCIwhy1y}rV_TiLotIx3d_wp5?F{peBYW=&13-bpB zQ;%Cr$oxl1ka38>3T-x#sxK0^y3YUg`z-u^QiTJSP<7q6G~MLSA%q%R;Sb%)N}sf% zMJpKe^v!9#JLRforI7p)9eO27gS}l6o7Iaxl{5VAKQ8mdW9#vElTq1 z2qlU6^2Rv$U1m@K4BxB+gxyu0msxpbgZmT!t15H{P0z3 zg=@yWPCA{z`;UWl;|fHXvMlxln18k8FZ|@fjz!`uR^y0UgIrC~Bj?IAESqJK32!c~ z6iF2f^O{L*UjrhCTBY)86}{+Nb!pA&%{xc^>G2{XSla2q^7!dq@OCyPhN^s7EYd7OeIZdp(dC~ zubaZ850B!QDB>NmwTYyzz?W`~69dYz9MEc-ki=munT{@B%-T+oQnlTDMSR)Kqr#%m$#V)#FonL?cTRwfK87o-X~VFoEmzfS20 z*r?(q)0KbizMqoFDg$oW6ELpEvv|ekPbAQrWzG+@8!VAp0SNyzD+zwsD*V*Qn_4Mr z^r=LHc=HvhghZZ`Kh4kSq>ClGd|O3QQOP`sUC?{<$hSTX-~1!K9Y@QNR=?eH_!wQf z+Ujo)a#gAUihY~@RHmVeJ5BE3<#^RWiDLI(IsJD&J;o$b8^X2yd5c?xZG?o^ens@# z!~iGb^f~d7-nFwz(IL!zzdIUWWmaXzQ#)!d4RHG#n2)Xi1$2yw=_(d=*nQ2=$l$x$ z<13%8aw6Q=88KP;7JZiq&*YP^`eh&o^MR0nihXK<&{ARKBQSPGvrH-Cp>LGw3M?kT zeQhXzXF>CE%tm;n{ANY0l81$O_|sM1vScmsWk=oHbnnTZ!q|(XPJbW&Vjh#`pf*Gt zkYdJbwUw_q%*tDNS<_u;k`UyYHS3oM zzqKGs-f;E6@@V}r{equ1gU&SnLeS20Yd4TrA*c|G&4CAwC^2Q~7d!i6H}16@;AR_d z8zH+~q3(Sr8qWT?v0x=y7tuPpA7HilGZs_JXPko*hAM?r?5(h`-#Z?9F7el6E6rt@ z`kP#m;heR2B(J!4HqO$(ZG-@)&4(awS8!MM>wbF#*7%X~(=zvH@h^qQ_{au+Mf>>~ zgPMk#P)ah<=^33>oz-K@>(cxv1(36esgU>H=&*w6=YphI4(+MRnO0rxcp?hYD_>sE zQO)klcyfwmF?4Bk5#^E$pN%kNU?vo&qrmEe9du5W18H>AZs&ppvyS&RrEKvsM};D6 z0*r|pm%nY4i(aCzFPpp;kW1}UD=9>-MSGNdrFvV6vqNsEhTTPTHE~X_)8#wXHh>7+ zz81RNfp4g&7yQB58WA1dSH@$WHGC;T?Newgh8x@Nt>FC!5Uy>t>5 zqRmmB+rt7Kxwh19FYc&mWUW}Gh1j9-;fMwd-ao}RA9Jn4!pb4MECRA$Wu zZle*U070!SWyB$So$%9vkLz`Lvl@TnqL1@xM4)C1z@)%~66XFOcaC;wi#la7Kj~MF)mz+}A>OS=GZr8{GQmFvsC>17(3{N|IV(pXVbK z9Au@?Sgssaz#N*Wqd=hD#m6p>XRo_U$pW4Yl8}ifSFYLou6Y$i+!8$GsY#k3Tr$EW z=p?x378cW_F-w3d;a6XjV#(Ez(dh;<5p)G3`V}<#B<$ZDm zFli_7KcV*B3T9*qLJ7_&$SS{J)s< zXR(zB>JG5OCEI^0%4y~bm9fkDDS$K`vbo}Z7w+Y+jnG@!{q%m7;8j{)mkKGUr>7E( zo7LVUJfkfbP+-T=gyhQWFu!Fg*!6f7QzMq4b)4^8n7MBN9opS+eQAIv^V-9}P@hZ`6@|p#vh6o%|stQ6Z=99>iS7&6e%ti+P9%n$hW$sNhoP`%M8f{fWo^ z$^pnMArSc(JiGmq-he#q_H4D9S)I~S9Newj~{Bm-~$705FCmV}SEt%m!XxGwyeW%L8Gq6H$8ITr*^Ji>F zt;})uL$v?Q1`MiI0j&=97iSS9nQrDjxTk+==1Ysay7j6*1roY^@>~9>L~@A#`8>6} zt$|_p0F@S;72!JDU9s*H(K?3*YVR7FDak$^8pSDT#oR-!ogeX%#IG`T^>;Pttlitdrly^W;t{hAfH)6Z53OW&c;cn06_ zlvIP)hOa$?@>k>MPkF82A@KggUaY$wej;8#=s zl=jUAtHTUs{~m4PN%Yq9>$uk?&yC2tT&^_UZLP&lx@mHlAW6yrvlTMUWBPvcstp-r z4d(kRsN-~hoVUy-Y%K;~+Y{MWQCUJY>kv?xxBX3vIA7gb@O+L?ulNT!Wh}ifE^ZV> z)LC9xNFzffNY0pv@Tq#K9L(Eyl8i+mh=dercHZ zMYIACB-bmJEZt7_?T5E3T&#BoXXszkfOia9C z9uW?~YVBjnyL@Tm+e~<-#y7W1sBDWCp?H;UX5G8KC-7h%7YeZ1wS%!7Itmpm)^9~# zm7~ZB^6%XD$5~RAT1d*SYG)i`7oevn-0}S6942fP%bK-)6@2bwynny%_uXVreE(MI zF**U)zdiWlU*)xh#tgo~nIg^L@jKlIz#r1VQJJa>$rV~=b}E7H7Ug7%ZRvvMPJ8u5 zF0M-KrI!tZYq*5V?6gTTpOy5zYKAc>reC;W3xRj1`HDT&Pj@e-Rl+`k`euJcVTO8& z#(IxBmzN#%zd4p;TBvU`Y9DmC3gjy~HuR}l?exF#S)ZxZIZtGmU&;vn<=Dx6Xnjxh zzi2G~^*Xk$%Be&>(^!!Ie>9f=dL9425t?iNCz9xY8JhnGwyb2UajljeX*nFvkkTCF|Bs>cGp^Oz z()EAxJO3}Z*3Ac-AMbUp#Bt{N9g)J?m zN9|0$V>{{G7ZrqItbOch@P=ac_{Mxe-s?K=Qbh{GC-<^m2esGhA+XXVrR`Mosys`^nIC1EY{p*_o>sd1-u|8_9*_$I$w?V)Nnex{RuY~Bhq z+4ba}aKs#Qcj|?T?Ar1;=s0TK_doRAH*QFW4zt>+ppwM`(x2bJSPN&TUCfTfJypky z7qT;M#~K<@&z^5J;<#r4e)oaFPRCS$nWqmAnd5he6*mU{d%3pWyQC#YU}ouK#s1WO ze<@XEd+qk)K6+_Kg$m@cqptq^y3h$6E`(J3`(%*VkTKzI4xfr5>RT;H&|&&4(vau2 z*VP+d*x=^6E~hg+#3I#HR$9V>hylv$R*+`DH4pthgwMJJKl!I;rZ@YeG-|M`F?LPK zX;nE@)^DmrtuXpayh_xfr5=u5ce*Ys&u2qz_9|Z4_%$9q=2nC2Ie2qjy@efI%Wmyaz3%OO> z!DKG`hHx$yWTbNG?0`XupsY8gNrB7JuLiGS)g}`RKlbZP!Xta*mb$~9s{&8N=JcWkLgz%%<;?)%{hguPDIhaG@pQQE)d*ElV&PsDCHYw&TTKVUheVjd3JB+`#Al z^Hou)vJ`zC)QuCsP(_$cAPNxy{>4U1SJDTc2mfMa(ZYY>J+*cIE{RgGd0D&h&OT-xVWpDE&XKZI z?hPJKW74qLG#eZ{qsbzEX=$PPC;IYWuX_!6Q}fkaC^4_-6Qp}s_)bmu$SW-O!ZF&A zKsD3no~8snudCspFV@lK70Y}u!Ke5R5wxXuE5jGgJE84vQ8`A5Jgh*IqQS*11k4wb zE)(EB(Dm#7$|UxF1eQZkd6VfKy`WO==NAs~h06o*CaaKkM)0Gr_YeU;Rx$k6e27-P zC9ac%DNNX?NaspyW>ADY5=&Fm3c}+_)&*UN7i^Yf_>9Fpi^6cB#IJdwMAvLwmFOFi z^^I>ZkAGqao@PwAG8nA&_@3~>HB9zQ4#6g=)LR#X>sW6;e(gGTip;_yDFtvn)8&5xo>6KU*@j0V2Yv$%gd!G8iBBwwfr8)5&uHE-zf{Jy!^95OJ%K zX2pl$P~h@HX+JAy+g$j)uW4ClTSq-g?1l4N=Ps>pZ=HL5>#=PeR~G8)uPn4GQ~MdL z7D6D47JIxHt|_bu3=YE9!kO${#vTzM<%*A#m;h5Q&_6Wglh>r0Y@@--Iw0e*@<_PU zJ63=Pfv1`DEd+fbXSX@X&y?r%-r1stkxpfA)D7NW$Tj zQhx!~R?6P1B*+Bl4U4Z!AQdgem+Z*|;2|X>4f5%Ce;t%yun-dNWNQ;!8(EDdArn0 zv#uzIb6DwYJKc~?XJ1obu|&VGFYPE@fxiwv_EYsYFJ_YXT%W+J{oqcY4^CmF?6+Fo zK85w8xo`@f3-Fm-p+E6Wz>>De%A8W@?*UjL7pSHjG#tmcm%&U=2yUyAVUZ{1`7I!R zTOqIVKuYm5@C42fdN-Cd~55eufVpInF2a z7NjG~;TfL-U$6qlXmY*@g)2ND%Rbn};=yx6LaiUS=D;0Z8MaEcc%@hQ6N~KgdM;z- zRYU8V(JsjTB^r^x@v!H^aGDeqHc6h_864JUAkINtTM};8(Ing|9SU#7%Hale#hP@J zk!r}=*?VNz6p@$UMeK%bYMG?GEkUO!cyRK3BWW;H9LPQhunr<`i$9RzRsXV*5O>GIQSrg*FE)Y7fTvOW{-%sUWNx`5c}Eq*;ln+_aw5Z zoj$l(QiR(XJA<}@)!7F8#haxT?*$(G7^IqS)U&D(>|=($W=7D$jZzUO<-5#VXPWmg zT=yg4N=BcJlW+wZ)-Dk)V+QVbOiJPeuuHsfJM-2g?P&@g4A5%WB;6;xA!ykRGF=yU zSJxQH-t8lS25RQ&8pX!H8)Y{c%Q_{f)+|P6zK^c}tJ0@9aRBg|O zVS~pDF~10VSsQeC7=GH#W`kkwWe>F2r2g8JI39cyM);fHeyL-&oyCLcdJT8HtWS)g z2-_E^f=fID@yKY{qD+Es3pT(Ph4`RSG2Qn#e-MsP!mz450TY$MFi&k)o$!c6)_x^Q z_Y=38_JlNNm!xc!LVH5R7QUQ4p`GFzg4og*h6Ky-w`DxCDr#L;%J|!+7vm4A?pq1M zVV*F4CREEPOBlugZ+;v{B#nmbGjZIc`RQaDvYCJRmD^bahr%7rNl6pu;uiz*!ztVs z61#q3lXQ-nJSPOF7VnBXO*4zce|hOrjaCy4<0eTkahmY!>@v})961)Zp1^UVziny8 z(Kma0bbK3SFso{Xmf0Zbsc#GXL#?LdTWC(+ED6H8D&|gFxD&0b%Zrn$<%va;P>9FM zKoiS=6|GoeHCc8;+QMb{phv-@3_g&G;W%|5{EBm@CT6ub7XH}A@)V4Ywy(z1jfS+% zih@uZbVv3-p)kBxTsO%{H5m3bfVc7LS43p|Tc@7hi{`;ee%g76-RW&K%qhVX?sN$6 z6>Iiu0uHSgmiq_fhIkYXS3CjN`^V$Gt!EEt(d-?5_$)G-#<*bN+6i!)QOd^oqeep^ zbb88g_{(5saHVQ4iL!zq+#`8~NBe8(4PNCX7MU5OftVS*Z-kj`NS^1zryC8sr45Bv zFH!lGkvyP59*WPh8~PeSw}N!-fX{MNVus{{E>!)uXh@mVL=A3 zMonDz@bCJk@#*{d-6?{jJcuNSZ)p_b!RkpSzT9LmfQNUEKm#GWP=d6g2$cguqjcQj ziyo)SaP&TsZvf7?GCYcNqiChjGC;HbA>!RCzA+_msH4NvIJzg{qFEcOg$PVR+jr#% zoWSYkak%M)!*D$Ivx{-u2EsM5&Y`#{|851Kv9ZKMIxcML9y24STX#`RY-6s6;3 zEG~`n!^Fcv(tQ))*2!z<*i9&Pvf1TuwBns)+YQO|9R>J@+G6Wi0ZtFfxPA9xs|j^! z@^EoH?D5!8Hg2?LD@;pY(gcoJNIuW^Q3-Srq@e-~k%Me=klA7HRU_b3Xq<#!HB3}} zdr;U7LHjOS09TZ)oaC6Oop*a7C3{<(WJA`u9DoL^R7UkSb_|P%?3F5SM1;Bma}utM zr1@eopI5IIt=H@y>cn0m99l3M9>m47TWS<`)`}ZLsAUr1^A*7US!-4Md=qXAhH+n) zF7m)@o?6%w9LP)&_IrGAPq=om*@RDQd?Us24>I^lJ0{`#&88prz=s}1c(yP5SXMrf z%Byak*xPEtd;8GS0`<7WKlE5QO_GxbLLa#dr+^H*!>fg7`sQZTi&DV9UEyL4g(hjI z$301!SN9*5muaf~(s;Izgp*fzDy(I8`+5qrT{u?B^M^swgJGH7+HUmF1@SSSIZIenvLp-W| z?Ucr}^RGw{6^AQYT5+oFWqov8FdoD?fy*d8fHZ{y8$JE;b0Ge8wC?QmRKvRgnN)~y zzb!gz@vsuhd>2wi`0?N)?0%a)H4OCN-N{swQ+hOR`NfY*q^aw?bB~RD=%L5cgF7`F zo%M4IXz@r_7Qt)L0(hrvt>*k9h^q&}kCLq0H~Xl5id>q#}I z0@uV+zmsUYQrMfosZj@e>k8n>vK6Xx($R@@>dYjcGam&+nC>Zt_d>&h&j-lP_-9FN z!fv{G7;5ws1dH3*tSNZBOjh1=u30baVW?v=#X#;L~p-rI)^=M(iOOnW&H(Y#`i1^ z)77&Q>8A6R>ZvJhKGm6AUzN>%#9QtaU4zU0~!^L_BKg<4CKJU(eF5%V z&Msq7DjE*V4u;J$p-oUvqAnCf;`_jzs9w0#+G}x|;vK5oqQmn}&B85ceP9 zLF4lKLv(3>06KB7*Q4(3EKX?7xkrdgdv`5kM z(l0zVNnB9Q=S{ehL9g}S5k<+xcb+%-{rLPV1x0cR$+$5Tx^O5x~L0SJ^NBK6?S>7Fx_cWcfhCSEoS*L?zZ zgs=6YcolHzp43k~e}qydLvJze&klhi6cNYB3{Noyo!&$p`KfSUa;=s+afp^tz39&R zVSdGQR9VB=RhIY02H$yJ@Puk%OGR(^Mk|B&UWY(HSyJgY-N3n1H{}nUnZ0j7zdcX!B9w}K#BiXr{3Fk9ussu{} zSwM$oiX>i3W8Y+>E;J7l@qmoKf+8fF%@i_aiWJ%+(&QID5FsiqE;bfQ+HPv}M}4CvE)dH+Xj6I%9WJs&TY!(bPovMR^#o8mg@+Ni^tlKLPbuoo-qFI{QkOq{ zx1{g6j#(AfT~crXNh#2_dbWQ(qzZ66X7~K<{&4E$lr)b|9RS**|5a5%7 z5WB;}-Vb?IYpf< z+(3SN94;;{($`&D7!%53!X1V3!=exp@Zg}~IAe{as%BQ>QbS9oAskOT&w7*>SST1S zPK^~jqk^6dm4(KJN^hz}*qBh0w@YwR(PVFDvG-F6cBx76Cqc`6A;RZ)N6#0R=-*r7 zNJ3G@l!(4P9{-n&TLE8tV*XdTh3Q;vtKdEm|6gd;K+K#wHP3ya20x1IfZS zLqWJ!?1We4BK_8}Aq$SJs1f^n;X#=NZxWRUWn4h`gVBzLD&4$MMgtmGg*OPRP~6P( z)#Ljk<2XAc45!GW^vh3?PZVb{cn_DxJUJ##5^xC$MKVl~3&bU?(Kc~I$Nwq`D?LmM zL9I-m^I@7Q!$*bfb23e2gf@9$u*ld|w5D7_}BWM}fusPB%gjQji!+8qqi1Z2_OMsyYExwuXX@sF*-3oiv z^OoQWN0Zk6I*NNQXsIs+E!*UQ6{xI}UK!$Lt488X%nbG_WnSqT1s+x%c*ldLx6&xA zr?nf0FM~z;w_gU23mxlmVWmtmeYpxRqT~6nDh6Lb>8x(@m58vus1}{oM$_6U>2Q-; zqQBP|IuR0LJ&X!9v$L3PY*BZ>Ch<5Jfj_PLY(v%^OoSb-!hJ=9_{+ZCr*p^j zkjnLQ(oIPmCD%d>$pK-Y*RwH70;xHcw-aH1uLS$24%>$!jnH&$YW zrYhVY__=Z*oaqU}hG1PY>MWybNSwn*ZQ+haBij~^bV;-h5A(Gu6}9VIlaw zQ016V4)+@!t<};;4$+J=D{!33U_aX-#EUd@w8{a5#Sso5;308!7`Mt`GrqHUQM;O% zG)&JBCUhnmPGu5aVx2cpH|>yYrB@YrBQcYQbKUK(FnM0&;jbo>lSt@{hGwq~5BKdSey^Zxlsi!D;U7~qaVTo27S5~nKHHxl1=M4D=M5z) zdnM{UvCiiYL1|%4Giqt{6UM-2&t`e06=7QCs! z@I*m}P&`meRh2*qdW*l8$mLZZO&5=CqS=GeP0(e}t$k7d4uq2f4Bbuc-N&ZjRv8!( zLWxgg!>7048aJ$cEJP6Gb4~&PF&{qjW1d=2D+hD;>5$&KrB(*KR#T(7;)}T-MX!frFyVnbK;iloJ>r<#lrbv&C%rqsP zkRQJ_8h3}YBgClnQ!)7-xLXEl;R?;#m)6stdHrf;M_n{KUy zG5B*R8O_|#o4vt}O_=9duVy!eqORnVzW2go0S@O@Dz4bu7QoE}+r<)w{f|;y)C3Ez zWEWJheF9t%lC{h)4N+%P44V5ZwC(v$f&nMafQU$-j>&{s%(ZKAZ#_KekqQ3WEuACG zii>wQav@P4-6%ZKTP$hg>*z&EGjB4KQ#_Gnd_68xTHp*n$p*_rp?2Xf-y9#%dOo)0hrX*!B)walXa z;$|5a+Z)27{+j(r5QU#txUbf8ZwReGouh3%U*`A|FQF0%R&}lPA;E!s{@pZDca}wiFvmZDJUKYuKM9tg zgtFK98k6QukAF$F$)s@4eu@i)n!?5O7Mj8>a09;Er&KQ9(>V1}7{E2bGW_!TDcL3~ z+u#cLJ6!yCU`Y#31>HFEj5r@UoC<};b_uRiWHo!#hG=qP>4ESs`uHDvaqFWz@;k6( zZCZJ1>YrOW_Oqr2;+)Zi}sA zS9qlZLEw9%aG{=1vxK^&gMlue#K^i}-6V>ZrDv5K=#`MmIdXn}GH}R_f(OIVu}SVE ztfmady$tVu

+iq9^?1+&4H49|iba*n#qXYlP2*Ik8$bdu3nUd;;ed3VT9}Gcg~R z!ha78=$Z#>V;!nJs|ZTU{A%{y<0EG(J?BAbL1Zz#U9@^Av(0vgtp!h3zRY(+7@p>=1ipceJ46TE+ zqbIVxtTCCsRgIu!r_S158Lq!x1EpP~q>eOhx2bW^+Y4~gTc-9&0cY&R+%dexL zPV3S(1&_^O_Y2LqIL&3d;%FVp@L8liX*MPIp8&U%dfAUcP15;dde2pvH{0;)Z(Nbb z3(&YYg}uuf4dWT3nUC%{5xjaYyt&Ep6lLpU z?W>I@8>dnBnJzP|qfV{m`M^impjD?CMpduY0H>COlCt zyip-PQ%OM8u3m#^DWwR)Os9Wg!ATVIG`DI>zi`A$@e|MVpy!sFLi3gy?=)SwwGZ4{ zbF}A@UeHoBh|tN5`4=BtkNWT1UM@fdC+Gp_ED3tQ#MPvDm6RBtyZCdxi@j*nC8}nA zP%MWMFhz{zpsJ;HT~O68y-Q~d7rqraC`ss%Z61JE&8%|qs)jmmeSg(lobY~8LLlGt zcn;({CGOH6Hz!67Cj#XdkwaLcQ06{R&;hGOjaJeuKinf%3jbhUg_9ijv5_zx9UN6P zVT)YD<=O)Ut7{zVZZ9c7EdcL;3;w-U7T}WiPJn@d>K2E= z$1sR`W#92WxR)s&)2CaeFD|YTWYZ77^sa%o*);I_O7zRUzAjAcTZLm{^#S2e3j9q` z+0Rj7tfX6iQ(~(1q{sW32af__V^wlcH||hq<9z+wD)~77pZo-o6VC|;e`W}#iE^FM zG&TU){$5W;hT}qK5WEDjD$k1nv9u754~^2_Ym;4UX{9{Qa~6XmLoT#558u_vCGBvI zTmVx;Olt7LDFGEc1s4;>Y&tFFuuv|8%ZtijWZ#vleQ6LbDG9=1bzWv7uYg_Bk=2p??JThZNy0=A9S7FL*v?WC%3}TrcTn z4{QrnPr`4S&ijRE@48meZ(2uH5D zDC&Xorje{H^NUb7)Nao&e0tYtT6i@<_iGt4)ICv&n{GUiLu|Dp5C# zO-l5NP#9i|Rk2Tnp{OSQ;bcgCHWRDs{A{QTw)!gI8cg2>x(uf5p%jSp;vsG@p*WdH zh|B6UVd+Wd<}!cWZ0Z{o&-5$nlu)a`*zAE;tx%KX-?sV_@U%im&3eabO1h5BPc9c~ zo5}|cKZ)QLH9zPJW$5+fd>Zd{9(c+l<4>&dYu2hE2l_N$7n4tOkW2ff${8*ANBoH6 z?pblWsU)V^#r;Ws`_GhM`~VNX&2DGL6z0T4e>!J2Z6ryRbbunjIMeD`p$J!cxTUp^ z-N%D0$%)0BuksgstOjjG+-ll2R(w{+%m%9{Pk0Ke4E;Kh)GbM=rBc=zT5M}dt$ zK;h$U>WU?VXrQ*dg+Um7oMWSMXq(DOXTJto;9WGuz6#Me>PD5T~$J^0cd>}8m>1vv`H#<*V|5fPASH}jAo0mz7NYU zw+q;1_-w{4_3m;zx_IRXf;+bcIVmBnu!~~2+N$sm7Z1gyadh!RlZZiOaZ6BUKPRMx z<+#<(7unIpb+`a)aAnEhlyMZ8Z^j z!TzPuk8VW+eq{@^`|AtZ^!EQ57VFopuYYNzRhinckTanNPx6B-7DkQZ2k4!zj;t2e z4Dp^f1on*qNQnd4p+)Qy(fj;F-~CZ|L8)s-o7dbrCk-aJzQVtc!-*|Jfhy3+SQ-a=gEo9D^%LhZcuA7kJ}GKE)d@c-M^J>$oDzZ; zl`zD5RSWBu!S^V}c645?WoC7Xu}WZ~tXVrcVb`G%KGL5lEkuh7j`hClhpRlM5FDhN zYYWH0dx;Ig5}RD?hdl1g-bPHjYSr`~iBf4C?5JHSEL3ttyF%l+T9iq!$}beBr_Kd= zO&8!MB--GHa9p}82=90bh;t|WZN7`rOUEhD(bviIt;qZ{?H#obsOA-&?B59zQ!^;K zXgoL|r=tja8(z$Hlx}{~xm)b#7x!27SR9SbCeCs2$Klq944+3kYE0B8dPnV9!bDW` zR?{>k{6wVNCEG;~$7yHaneKwSTIKOyR7Hb`2i%N8BJ)vT98^@T6qdG#_fMo*j!TJg zjnYvvEVwno0k@J`c0wgQ*pPr10)(lroJ5eWHD}O&!C7oL{q+8gc6L@g(uJkJ;&HL` zwXvE!!XnIEERdNAVPv27DdBKvW{|U4 z2vgsKy8N>US3Hl;{#0`YWyAwCPI-lEDxB$uSw#u1bS4x)QP=<<3h4CL6|gT6RGl%6 zQDf_noS9G^?+KrB*n|cpG#MjYDma!=J2UINMj^0%@D+o92xKd)+Hs*TTFtB(Ye;C*39~JWx`Y zbcb^yS_!H24Qunk{2{npfADeS42^_rTLt%IYGOCWou)w<&AHyi&8&t+5k8_du`R0o z{B+&Aaci15vv^udN_2>~B6ENxZywadYT2|A2{XOf%azpcJ`Ro&wlm%2XSXS?qGq07V=SmMBX z)LR2f8b{!XmZ5S(b8MIZxppmc?KnN#r`C^8kVrar72un8?~|s`87_EA{Ga)^rU^M! zQ`c2SlLzAGo1}WSScG^mftzPnn+-YY>|@*DKUHBZb^cMFyCm4`%;04CP3Sb6C@w5t z&PrS@JY7y0x^x`P0Nwo5cu-A`Ix3OOYt5r4vOPg%kcX)E)hT+^5@n}~kE$DcAI^iX zt2C!E(FN?2{?o0W+sDHfjVrX&)8*n~f*57!3^jlqyBMxP7d~9l9#1t%M~DxVajn-3 z(d29mZjq`u3->=h=KCrDT2Y;V=a?GK9)nMi>np{JdvgZVcLAns^h~5o$yIvQICex1 z$Jr;qIF`8`Wtl_Kb;~fO(UG!f;W+kRupC<=EN3PYPGpiz>>ELrpsys(=H9DS&r}Cp z=DrfaIg`lxaYl%^OK8q0qH*A|GXo@h41LdDwirz$g1fW#`SlL3M__x|Y!c8(TM(7K z%zvmX){$Tpw#Y&sT%R7O@JvP(#EOjy564x?J(cnsLej7^`vo@xgHVmXc;Q{9Z~)?6 z)(vaub9Z@NpK+I$BP-X)aF4f0CIck3Nz$YJ7ka)8{4a>8voI2CS?$q3yqyt5w8n*q zHh6r)45P`sg{jsi@(UDlYxfZ6JdeYCj6UVlzJWPh=qbUwQsH&NglVL8hRcNhcZh<3VmCOLCe1yd*DQOSl2091NC-*p0`Tw8H~; zhi1W767&5^n^o+04?#qx^Ro-?E(o5rB+W63%&RVS@@vVZPVV8K7J=&TDnzwAdpORN zj4Nl*cl+9-JV)T#BOJ<3z2W6Cz$wA57~qs(D@w(ID5W}4NE+6I5m+vjN9-W?y+-(V z(NT&*QqW<9zf9ja!oNTqFTt<)8p04#%`P5)D{+19 zD2xe5#Dr%HWe?>i;lV-ZdgD-_^zCpztq>4;RPhE`G113qGlhC#q(S zjmbh;5BOw3H`C{9&h+^J@5X3IS_eWWlX6s_s<<~%qgr#zyhj!ibkba!gH9Hey3on` zvQdWzn= zc+&3E?RFPDY4^i`xC@?)B=BTpH+a(Ec_xPD`U{6{SS|Kp9hb2 zgD2l`@MJ!GsoaQ8S|TCNt%ez*q$j^a?_ofII2xWK#lSC|OZ263IqIQ=VNVT!<#l2H zFO?0d@k=2y6>z*9zIb19;S1CqQT+38?mMNXagHA3WTaWTPVWwV;!6t;tF;na-UaWPK_~L^Dk9C0JCymW zk1K_IY<5HMW-=2muc`wfe!W{&Nn6-+7xa#G?QC2zfXu0 z0CQfEo;tGk!@@%be9#zxeF4hu*aCX6HxH_{maS0Dp|PmB$wm`GR@0|cB#iVJbToo4 zjfTQ7yU;$|9WjM9<&exJ#4L-h*D|eT;sGI6O0l7t*XD-}B?)$(sN0`6-ajYH6CM83 zi>!3ZHX3RQ>6LvJ;n(kBBogLhS`Sz0C}${qvub3z$xe3-%(Cb?&3K0OV*fO-q7HJ) z4NijL;g@dci2LiAp?LmdY?jych_7tMgH~@Ibn7HE+rD-HK}R)?#qGkLWHjJ$o~b|=4~6cDAsXm~vYMGh)-F!-#Ml31;541^a$t5xUhq3mf%>4q$( zsX@4kh4qC;j)<%2-n|l5@=nT#D}y6E8--tnikCK1_zho3e>`%raQR_)y?8$EhCVRi z=2oZ2uVy}LmPQU&ZjnMmg2kEKK)dla3aE>3R&>#&SkZ<;({;mEdGNJzH_TV%hwUzW zVHw*!dLDnGDBDOvpy1_V`M_xUClwS;FEls?a^(oLA*{j>|K1XOXLm&xuQrmD0cyOJ z4$R4*z;7_dOgsp6bZtS;=VdPB80ry4pQ_}U6iE{&lCLK3Of;iyVht?{1e|d2pC^6^Yc|xi#J**z``FbVF=Eh{EM5%&Bkuqp2 zUnx8>PBX^ZFuQ<{3G!&9#S*M z4TdWs891}NgGj!~YytWX3i^PqCU^N%wXC54T3wHUbCbw&}G=^aWN6{1Bp=O zsnwn0YQL8GqEWZMXbh{k5j75ARhhyUjhay|;h{1PKYm`iUd?{d=zX2Z&rHZO!YQryOQe2yFbgHN1(OLcDV}( zn^m-0SnB@+yCyjlr9z|b#$9I>bqH6_6i19B8K7Fdb#?(KK&JCFx<)IL#*y5|Tn97d zXF@pVVB(*Q4g%FWgcq9JbWWa=+hb@6T5Vp{SvriJtHN{W9S}%xMT9#LYIc2} zX!6PsAN&7Kx$6-6WHoaDITgxKlmUOF+^taUt-Yk@iCiJWH^njm1la?o2S+r`Ne%Jb z+}I!llAedlacdhnik5l2k2{KgQe^@LG^77?&3>{kX*V_V(bT9hVY<@l=L-4rlk*DM z4Z>S7^J#ZUUlb*E95vkWo9*XQ?xac{MDXFNnVn8B{F*X2L(8)|(-5xYnB?tzsPcOMiVj@HXi2xL$@#-27s(XY*NBfqTm{bJv!!koZYH;BxTGJ$r7ctN z{;zQdcZYM2?yqr|MoFtGkc8vZ01l+w6*%Z3Pu9WNy=&0PlX1IqD2n<&4+_CQ;;u4q z8T@@fOEmG}`FXTvWqi=4D7~`{Llmf2DK@@U*m5C^?5E>cZ4{m{G^^Uj4S*N%qL+Bu z2or#tBZGNT%rge;JS+e&g*do5G~1NiAF76S9FX^lR~DM*4a4c+orFG&ne1B#JfPK{5byc9^z2JRqkk3lmzn`wAS3M90(vL)dgt3 z8S>@QG?z>#UmiZigQCCAOkGb$X7pfDnZgx(vg{~dEgb0+2PTzCelig*h;1bHly2R4 z1FC_BVm<6HFPGwA;tBl}6{{g)_S zVbTcCyg~9IF{0q?o=iwWiEU;>*6jJ zwLZJPYi6vPCpHa2WgsPtQ!@^j7i>Ax=s;PtzgPU`Kvd%#CN~<-%d&-2$0j5w={VyU z&8JC*(_lEL-S@MnFTh_V`^(4%=x=4w!u{-wJS4P4YBVVOsKWY?XJNf9g^0}^A=Jl6 zi-ak(PvRKU84Ul}B6EkX+nT_n2L8v(Y{kHaNY1%-P9 z?8+eA9Gpy4z+&U(AP15D93&8_(X_7jk8`OxGs6EPJMGx8>oyBVD2MgyKpysjP}!6KY0N z#@;HC!$ve&%5zOEln(Xk6f#Tw6ziGTn+x!DP_dO{+MPo% zU+LeYbjbTS*uND=z#ALDvGFa+=tkQp4_D=3aW%ERSHj3_2Li?uM$@JQE;;4%2oZ~o zKS1LN(=PVFHNnyN)%9#kAL#&y{Nr3Geo2gn;;jW$IpMcLb;i|5w~&!53n%xMKQC8a zVxiqqani-e`IW7wwEfKCjmsY)ugor^n z{y*07+7dZrdn6vscFm*`h2k+}4mDaf*;OSLH^Z39T&h|Ip-u@OoxbwT&2kPC$7;_FNrvJ6ga1Mf3Euo|9HB* z4({vivM&f|{cDiViZ&(hPLF9!HMn7wPJH}i`o)kR=ZqTx-lFuzxg&!f>=35()$Au? zcvx1A=>ro+>h==lrBFl|FAwiimv@&(rU>-z1IqOfD6xLl{?PsN8)S5Yhk}D?cu6W0Cke1i>P>|; zzGp$WK*D)e)JU@9dzOH6C0eI8Jmd5i`t;Q@yJYx?$?OhKWVa%%XK-muh3)=}n{iOA zODgLirG%t=6n*_Y%IQ=f0q#*MctYIO$`T)-Ig8$r_4Pz{PCp0~mPuYOWJVRhdx;fV z>P|&$b|^89Qg5>RgHU4|mpTyk%5aB&JiAW7k3obmTseX-sP_^ZN*wfA9}7}God07+ zYjv-p1|Q_Pa4-9-g^s?WdML*pC>?G)(CodRzV^Ju*Rk1=dugYlYV*f_vCEu|WubC|l$%c1^D z@!n5@&fz%iUH+_>JuR^x1xZShfF`K&KqT~Er=WO@zV4{_ELNHr#?ZZ7se>l&k`I?Drqz^R8xPnBl2O&E)88 z8n;)mzpFK75=G^J5?;y6cYg#o6DXdf4XruYcO%*JMqeMt3XFG4LkCw`Fda8S#qC6w z@10`>93s@s)KZ)Jaa-|nbOp-MW!z`N4mF^e{YHg*q+pa6ji$d4Ux>Vbo%8kV^-(|m z$;E@rPe$w3eildfUWVe54xTBPozf5Yy@H#HL&}u60Y8-1s%C0n)bh^bSr}ANVds3& zogZksa7`*aIGp{5{X$iA4%GlW)l@q#}zb= zplX3%o(Qr$8g47uAT-yD8VU~$_fS0TVkg2aQUNquYgOz1x`bi89{`^(Bb_0^b!8Gf zE&+V#83tcSKKMCY3rA+_;ota>FBDMqg#w=Us-aBUtIY5<(3`K-%zyUPvQtI)!ZRAa z@vm&AAk?>72$)xld_&eJ6rwX86=&dIP2s&l7_~SK0*>{5WxZj)xFh^`IvXw-=&vs$4D1_J}MT_gAVN* zx0*hy6@NS*kCpcWLz-xyG*oZQ;pY}?qphJ3)^ON)-x2i0wJOsCR%1}$gr6mP$ZG3I9joS7# zRBWXpOs{ZxRJWBZ=~2-~hU3Dk@UD*wi#>HF9&I7S$4-u{Lj_p?X9jKP7avVLDr-bF zZoYtuv3HjyYz?Cc(HhpRvAz5`J-9-GAutQ|;3&;H@hF%)eLPHvQ-Pqup85}#YR5@m z`0-B#FuB(XEpui+@$De|-R~7%KV7qj6f18i@RVQZZ8RJs!&g_#fXy`wm7WzdmgulQ zn7~O(-`pS^Unq7Zg8Ppzq=$If-<1XaR9S+3>W$iuZt6m8_f23qA-r{@xI5Aa;<9Z5|ef^``a=3tb|zRUP`fXJ(RQ}Jh#A8?*|r~ zL5smelec78m#;J2ArR*0Y69%YLM5a3qd=oDV?Ivi@p%0en%&3HO?#lf(Q5kCHvraE zxQUQIGZ-4LWxag59rLH*fmjDs^)t-rRC2>BYzu{BeiSUlJ)t`^NNw}ZL-NHph@Y~p zD3o(m9$_=Luuo~LOqvse1!V;=zSmmKsi+XgTn!)MggLD&5nAsrw$Z$cq5%C%(GsW& zC)wvhqm}F<>o_yZV2EtPT@|C*(PF|%{~X2b7ufe~sBLD}gnLQfGk7vu0B^=th{xV3 zeiEQ+X6%L{c1<<(*L%t1&YuPm#+v`E$>SaFwTTBIXI0Phkxi~uUey-pJ?|RJ+ z+Tm9!z=`ms_hwm(YM-2dwrevmwzypYd_aT0OYIvZ=0XASewuSlZ<DXf>#8%g%yeS3lmWkJJwX%EsGZ3!h$AMf4)eva2}WX zS972z)x^2|lzGBI17h6&iQ*2Su|lyuRuGCL;bxItD8bdzWTGh*8&|t!cCL~t(atBg z+)D0asU8r55=;Mq3Ny&#Qeg%W3Aeh@wtomzMf-1({tp7owopOK>jWIR44A>30plCx z3AIW~cx5ZY-ayeS?`#L}+Ol7iICIxl22SDt)pQxo^hmg0C2p05qlQjS6L94iZWWsi zp1zA69@*RANl2$lp!sLzb;4{kWNg#;D=n3sJgF<=ql97I?os;<)xsqG!3Q26kM!XfU;ukn*BpRiMVL^N6rDgm)3-GujLrW~AqIB3l6^@#GHGK0C@bhY#QmeajWJ4bOQv0;4)-N8u>cEPv-ogpW|NUfQyQ zDT-U?=|V4tT)NP5sgt7WH;3Q|kGICOR;j$+Yc4Z%D893sBM))3%PJv5lmlv3a#~c` z%X;|V6zIFC0yh%{ZF{Qzr~m!;D*f`mSH*-!tAt;xWV4X`@AqpeQ#Vy||ND+ge>~On z3(7@Nji=L}s^ootP>tUAYt`g^*Y!v5``NfG){>z>ZugJXIk&s82Ho!ZTJCn27kU3( zNp82mD@w#+O|=YnE0oW~O$h011r3aI=&9`NH(@j%MtAwy=jQ$Fgl!KB=RNIj(16+{ zk;SXeU0WR>ciUGLhPBlR)p<{bo-X&gL|-*ErZ{TcSw%i~qtmoo5y|JiPU1edutOJD z7sJcaXzp$cwS)T(x+)M%StZ*RD$j$H1Q>`n!p@Zbns z?AEQlbH<0TUm;H_?{zt7Dm;F`T;eIdC zL4{;(<4nMPFMCm9<7feK42n}iH12WZw6WLqhJA_~+no_!6YB<{cKHv=FiN7?nAQ3d z$)kfEMK(cDILQ><^y{8hu$trYMumw7vH(iL{n1n?fG_nGTIwVd3j2ogHM*vI zRH{+P{;GJN54aX@tqV%qsl6I{YzBO%cF+&~i>efQ4=QU}hkw!SPLEqcmDa&2$xtpi z=n=T8s0JqH(&UK&n7*`$OFw5{pbOR19X(hL^HgHptWb@O-bOP!B@u{-i<#*vH=}6ewF9YQT?P_+N9ku3? znNJK^Yk|Q{vpdG;O|zTAT_qGM`aaEWRJyg|IPDJ;mlf$7GU88~SKpmIkU|W8c z?fr-17g}ar4Ikg_MXT5>Z^TVZCOwHd$@WxrRb;2YaVA#Ed0(dWmsaS`C}<&+%pyGc zb({^CmaG)$B~_?|$Bj2oG3WH<%aJYg{&Z*VC>>70ss4Vf15Xu;7jk8JN{*g0#UI9t z)UxYJ8|~EFlFl)h=X>kP=R85(_`92;!C7ASOC2qQwkI#~1o40?6twU_)N-p;v#o*X zUyJ>D`q#~oIl@s%Q!mhB;5q< zUHFvemad#lU}7@+uV#2R;8jgyG9HAyN%KlEdQXMcP(0_Z1B1iG8a}ADNE#Rfd9}&#{dU^`Vrd|Fra1t8|t*89TgfM*4 zoz^$c&wQ6juwY<9OFz?~ryGmNB3lh>qyhN+QLsI<_N!rvGzt9v6>4UdshgMf4#7e^ z!H)`bmI)NL`YFWqlw~{>fdQze!lyHTP&w2S`6)!N>8B7c=5h16acQ^$yBZ3htaWWX zgVu#+tSO|#t*BjtZ@Zze2a*pfk68jEpv!QmMOWoDG90q5;efI`%oCyEM6|D7Qw|Z9 zPldNUaSqj&hHd!Y^wlslj!mLtMN!pm*r5_+86K7 zs)ai|Xf|hDr2a`J`M;Wmh69?-XqCUF)(II~+}{DW#1d#U6u_d?fsjQ{10p5XGXI!_ z?G(hg&$lR503VmF6sAvx#WMpaBgu$3QqCC>=t_`J`jlUWG4dE{3G#TzoA45rL8ioA ztKw3zkE0ob3)X9yvR2|5l(lYC)2|(#urpDL#J*8zZsc z25%|>|7{*Fw}L8LDtXs?Kq+n%UY;)Mhjif;OA0!L+R5z9Aa^ZKZ4u9j!!o7I4tXfY zD@gu*!n5oOFPstXi)$&GmsT(}ObJFCg};s$T_L3#%6KYmAjIq;H8}|d6I#=7XN)6m zB%8pMdbxU`!o<bx+hOVzJ{+`C4tckz{ZDV|nZcxq2gEU9Lm-l3y3e^%J0c1ORTjdZ`h5={VJX=md&S&v(K6>9%b>cPxh7BXh~ilQ0`QE7T}^d`BOY&9sP&x zi^f6Afb|-lo5LW_4jvFC|ClGN>(1={7V@r8`H0bEWXbljGS<67uV~@B|D`z1n!obmG+znAZ-Vq!UYzD4 zQQjqri$%CjWb6Z8l*Y1*4+Pxj7{DoF4~XX!u?ylkNi0I0JBi%x>{Qz2c)PymGS=ch zOuE)+G6^xlyE%+X2bTxO3Ux-7iX}_JGKo!>VWB)Zmv&~7qikFII2Dmci#X*N4?3xb zgEZ|oaty)VB!&Lq)5o)!*F9A7DcZk8`9IK{BOun&pygJoJgIyo;?l@o#_1)LpnZzsot<?GCnp=jegv@0dQfRtpdZ9*v=D!i${iML5D!Ce`~3+DiU+ ziQ{Tbzr0$D2`N<=sL6K&p#pkAWvbp!sDN745QW+T#$JsO@M~rI``{|cI|&UKJF~<~ z5J8-(9~bcI$H$f?s%Ac?$s-Dak0>cK*@TLHZjguQQ~65BM@AcGRGnBbhOP4B^6;+m zgA9%0=K{L?-^d)?y;`AAeS5L_5DG0(5`H8Q(KMWkeaf#AC?XLP$3PQL4_ku!($%G z;eneyT|xcRJsf4&t;BKn2z~u-WzFJb6Xk!Y1tC1FOS>6-DoJp+vczYKU}&{-c(qJt z76A$d6v#Fn?4z3?f-peBV|2q5)|f%bfKIl_nHAwU_}(A{3JdfPYYHsfFbvaa3KX!f ziy`Kfo0CmR>$wt&pH$H@?QITvpGnZW~*TDDnEI*xNn zA(zxtPWORG#|ZLn4k1rd67~^GhV_WN^%;)q#JQ@J$9tbm@RmGu;{8%C@b;j9>-DOl zYR)sp6CB;obqvMd?h7@e>PQNE84wOZSwqXBD~0l)mi~7IsgW6E;bl)zgF}MCoynYq zw>#2;ryWXdg=}+&qIYEt8SOE$>6S$=qwE34x<`aFaT>j%x2u*`k+74y!`zcZw_|=E zRojlCxa868a~EBCS%BlVaW8>Wym7TcxOA(n(X@_}(6>$lE>=FPw4# z2;phJ_=jW>iWPNr{OZA}+k}5{tXGwuNCc5QtOnCLjdMwOWw(R47UM=vkWaTyH%^P{S>f%a6)o5xVoXM1>YNlDsreqfvIt)8z7vRQuw=Xg{RjfztN4 z(eywmnf5gNBRHn<$WVM|dN3hvQQ%bcaW)QEJI5f>s(GOo-2z#B24Hi;H2BstkiDR= z(|UOa+wcMz&9Sm-s&9QuzXEvHzY-cm&8L>YpduK76)5%Yql2qx^h%D(^pgX@T=5V zrpuy>W!;JeU00Y*3`slH5bkp9-8NyHRqHqv=Z|ZD9;dDxfzsJ*4j(jmw&1j)$VFy* zf0P*ptSD+%O=TMABG6dhBDtQ#*&*gz5y3ZF%LbP>Iu5}XZpi|)*0v=Y&2&wzpQ*hh z$x+&r{1g&y8VAd&$wX~uv&JUO^n-Ce%SeFSFI~*5KWfBK&2tUr(jP z3ixVVtVf#f(I<4J`M!62IWOp+y2E#Yojfqjclfbxns4+3uhw3c+9bk}htK5I{MixR z)%@p-ws50HoF0IkUfGwg<{!CH*3-F4{_mCXO8&mK5N=jO{rDO1+)i>R$jK`)tTg>leD-t6QaF zEb8n=?{;=AYo&N`5R~vt_BSo~T7NW<7IH4*orN936Qi}vN)@WmTgOAeXM94b3}2W^vUQ9U@vu2;Bm|Ex>FFQsig_CQ^ROEyH1x@6t&N-Gxt3Rt>ubG=p6Avi4 ze?)-9&f{tq*&V*v~cI-=pyAAe{ql7vhiX z3%HfT&y`?4-uHu%6>4T@dDLz^7A{AbdBY$rIk8@OEELjQMvoS3VLK!|ZzyTxo622L z|6pW&+-kbIPd6K|)r|usgRYx7k2n5|Iv*Vgw%@?>sWi(Er;EoFa3}NlQ^n#la^%3? z$%bn^@L1cOansU?SCKu}c~Z4v69|Qvs%)bKaL4*?9ES*gNyEHp0d~3!?;ZlG6`6p$ z57DgXdpMWK9|cya))lIk_XWMI(K2VR%Mvd-mKrshClMLL#DNOo4>aUY1X|#g5QEnX zld5&kgoK@1n8fcrAq;iGBM*~K3BdIMm%(yNcP{CaeXx3d^^m-s4`+VZlmF9{9u99Sxwkhff+d#cfNc?sD( zjmgaDIycq&f$A0XI!=Po6#>oue294N0wwLXA2~YGOy=@&YOQL2GhBBf$ERUcjF%3u z>LUk)?eb9DQ@4o6wZf_h=X6n2$UKfOg;f!EDLhb(R3wg4}QJku#=ZJvlq)&`rUrFq34inzx!#(p7RKPr#yZ<5X-o|con)0c9k|61PZ zrN*%a@*dEIPS*uAhu2Mf0@j4MAy1J&Cv~S!htHr)fBuM&g&S3&ue>e?%QE%}oZwC~ zZJ;+_PvF+TKdkcchXHh453zpo;EP zGzA?ur#zT<+-&bIhvwvxD=_)i)N|enMh)r$nkETHxO%)2|5>#nfN!RQ9MRUEr z+l$?)aeFZ_R`8)x$xF(*P@~rhQ4->iuL|+t^aA^~($FnDB-v^ta{1O~P;?RhjzL9z5*pZGd@ zN?ut_*U1Sdb6jxtMKjPf|+7I8CC)oF_F=ZcHl7i20{-uW0Np1tP zb_T3$BTl=)uw1g?>)w6E^DHg3ONLXWT>cbJfnu~$wk%FjU?FvnGMjj7Mrl@Cv z^UEQwaeZ9vdM(vf=Q)_ewcMYFQz;~PxM2)8nk?gi z@nXq%_!0e+N>Ti@NcmBUOb-g{hKp`}_*F+2rdJ*9?LS30ZHAhBOa)=WWbcB>-hV3K z2b0#mV{NI#?J{_5at`Wf>IH+Rh;VZ+AI|b#_}Lq#i22rqqo#Ku9nVdxm~oLPy(hM( z4nR2?rn)G{@6){hnN^nFB*L-9Gba(sk@>l>8|HX^P#4Vcpe*@&!W@|#%(1M{g*i3^ zaI>23Hs!3YppJ#mr~3Z^brh(n0m@J(YZ20s`9F}3P?1+nzcN(1_ZXNaN}BlvtF&CO z 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 z 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

#84I5c!S+XrcDuTtHMwr*?59I@K3nn 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%0AMUEmSCXn#UD*gb>J|ID;yx4=s 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�pyPH0|Vh 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

Q225q)$+?Zm_!9&-z`nI3iXQ$L{_K?{^I6EsOY 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 z zTk$=mURE0S+Z|dy!84$DgO(V)sR zprq%KjO={b$%w@1ZF> zfkYTeBG^7{0eHheKhOk#op!e$sD3=arm1Nwehf|DiR-uAEW@F`KjQr-wfQ&n1`pNn z$oq~Rp}+V1|KHW-{|3D|ng79k{{!evMN!ra?F+Jxtu7Ol&@zSrS8<=v(!k7I#S_7xqyb)Cjeb?(E0eJpC9^hqyeDxM;A__i76Y#;Ln(CofclRQ${3Mc&?IwyOr!vv zSYu=1qv+Tpu`oWBA%$ev<-vR{2J;M_dgxrAX8~Mo?iWP@q9HsNsQr2y%N^ExNMFtlP?L3WpT%%UjR?y5R*6@_Va)#QTvFzW;5}=7 zmAbge@$mX?+jZnzDk?OatFq+aqS*^f0vKBmfMuWBbWjnI`SWQWC63o_$Hf@T2+~}> zxvp|9a}Z0WGnbC&!861cwk@t*IX>qmg-*x`NvIO+5(_891pSiBM?EEj%>bDEZ?DY}+>%jAG7yJWw{-0d%p9P*5 zL*akV>ecrHi_9rF_>*`2UT8}=e+Se%)`a-pG5UW8p5GVxSK#@7TIfFqJbMpy_2Xry z_H^|PT@b>i7=tTDcBO$`MZWi@%tWqVMLM8K-^_HOL-lFHp=pmqNYBnNea1$>UfPXR zg4?~m!ok7D-VPbr9YIPknAZ4$ zZ?&Hiqr&4%F5bsb)#QAlVCJm(fUR~CxKcpl9^PDn7q}9B5AFxTSA_5@mycpHeR=195$t&KsJB-s2jp7;7P?)SdV z?VSWXkF}waGb^R8^+nvzwC(iO&)xUU#6RDUCEUl4t=9^|Bb1$8cc_ukPzswy*Ac%pEnoZH`X?naKAIxbKmAA2A=hG zY$yIXPbzF;CPyF1wio$L;MWLsvYYhV$(GgOl>=SuEi!_@Al~nhYiWl#y4QHAe+#ge zyC>q!{>vbx#Nx^MDpf zX1G#wx*^$AvW6`YyyH&0jF=wwdTpQRjC~@3axj9ZLF)zrS-am=_w3okNdtU&KU+vZo}(URtqvzV8%_=-4y zSX%0#_vWGZ#;>P?w!zxljv6aIXZ@DqpR*qJmGwkGYVc~8;J1?$Lw?MsrQJ;r$J^d2 zgfieY6>m`t{5ZU}o$Se-acRczq14N;)k1i3VqTUE#Vi!CIUy@+W^`Z1rV*kNWGe8q z#Q03V7N-{WgS`%y#!3@KHcg>$b&LI}HhFTYG{7_!RRErHION?jEYxG#wFhfUn&jgQE`QgFdeyz#|pOfxN;Yio*n4Pi8& zlK_!M3)8#m+ZSBVN!6`a}dQ+bEAnKo?c z#D`VSZ3f$&qCZT3^&W((O2h$1Z}o7~ah}ddUm}hF?C%Yq0Rt-tGLM6l(34mj=zMLOEjkaEN95^pgTvdQN!#9uVH{BF9?ddrgF<}3tSQraAi1#Npy@Pe z1JAGxe!mt+?;u{M2lin@`0}bFPj@AGvd}!(p0d>#F9P#)ULJ8O??Az2ml^t1Qagy=`m48O|jXXW;6Ijz)?I?%o6vQN@P7j^o_^xxi~6_E$651bPshqZ@| zw^MJt7$t46KJ@}UP>;gT5mKihojSRY(sSJP0%u);y5R5DrzcpDe z)tV13CAogEyG3Hlh&FbBqSC(Z?|p#-O;jJ5z)k_Tp##FULEEk)qC4xVIle!d+K%x`M<#{S{=>(hOMn`FS%ex)Od>cJ zTVfs@tb}W5lw=-`o9GrPwoF1ahq6`hmALH!Z|NK5eX||1;$pmX$faMnG8J>WG@uGV ztjIqm@{PcdSTWKNq1K8~(UqB5(G@4|BOo%a<+y^F6)!sI=T8qU;_>MPyX$lD(&xr# zF}vo2t7iBI97YBAjs%9<`q%Su^-*v!PB`Z+H9F!uImw?&iQmV4{FmEe`Fc-o9$^62 zFneYQBVz-MQc&|U^(!Eaz-6J>^{@32wJ?ny>S7-pc;u4L^li?;RHd(j7k^$mRM75~ zAqBPH+MYWhjo8+?Kc}b1cs_>jKh`~H{7FgvMiJX}SathgrEGTqC zNJZhg^>olMdAx5(J|C0ZMojRCgdve}GimZPF5=)uUXkn!q~NZhW=zhE<(LkuSE2Em zsTYB|kRoJI-C+tJn)*CZ3kTW}qxg#xx{fovZT2&Jq)y-n!WQ_&`%iBi?7 zD4iYu4V1^oBNaxa#J9*Ro;#YGoSdA3&1OiKr`d6}GCVVCvzk*+rLf3V&UV$oZgOJ1 zb!N8J)8~`MD#YHz0yEAHiA;f7SX61v&YID7a{Esus+oawiov>vc)qwzW}6g!?`X=N z94Ms9k43!pc&kNuwy>#Qut7aAgvhvA}cLH%6Xumb%r8Qr0v8qiKtXQ>mmvLqg1XIcN3s&7A+j8p?JuCQE*9NKQ&sFN( z+HNRm4R1%47StQ|VB#ecdr^OrbepR!pYKggK87L#L*eD%49ls3qfN)f-Dd zlHaepE;KpAmn{_C7k-GQCNXCg6dN&U-7bOoj$3V_*CAr{GRWEP<=)}um*b0^4k2K$ zM!xnrefYTGAposuoUk+r_c6yx1>c1lM&x9LRNk>VmkvEzoh&p)L#52nWPvF0i?QQp zrF7c{g+z*l8N)DGEKNOU?x~hbPcC_hV`Fkpjv7-w&m2W}cl$;`1=De?nQ`Op1iOw@ zAwJ;e_w#gSCkulmwiLr@(O1#e+#Uy_#)ILpUHzDZJF=2$%0p8r^uh$nXnnm@JT|RI zt`okmQ!$Pls{Wf|#FSu980tXX0%(v9oTX|G0-4J-cBE!ah0|Ol=ml)y@Iu zMQh-+!)#l`t2y`H@`)Tm3( zrsXcBZL!4KnW=A9u+RNy=z&!B(J15omt! zN@H+G#Y|Mx!ihM=;E?#}$2m0r8W0P|*pC7p!?>+B16(4-F3m(a>UD~+S=!ku0-3p5 zaE|qXIQT$NPzB@(biq>Rozv=mrc9YoUz)?T#)S_C>+nCDI+{A_>k`mAFLVZ{jZ5-x z;mR)RtdsLmwosw;h%&YYoNW5mXV!Mi^6etsc2r{Pij=fLYV)h&_MBRA<-3d!b|Tq; z;{lf_yXF}LwmiDEtI1^D*oA$ZVA`Bx`J#X_)fz;p(aE>XLP?0isTo1Z7hH;!Ufk$D z@KewuV8dfTJ75#z0b+nJqRch3EB73wBYW?q<0En4=p;P5$yjAp&?E85p#Bs87Jq2e z8_Awf9_pG;OsC+i_Jl+xCW5%p?QOS!r=$1y%Hzk3fCw>p(b=QmwmN`xRKB5o(dD9t z+=wGs7ult5L7f3tsA^jad;ny3COxu~k@oi@L3>Ca5ifqd06pZHd5(nD^wa_tPi6F; zLhm_)i&l_G1BB!A<9a{O9PFg24iX9F7dmv**X)6}@i@d0tlfkR*wnR?!4y9NG6iF4 zjHA@mAYDF~0@jNH;nTgUQ)0P@`M_?U3Zy)GYDlaj9M~K>h zqU71O)N>qi5%A|Mo7=EXpzc*&y?lT2G=JFopD|e z6<%=6du7&J0qAJ^Sm+%~#;BMbcqn`^*d8D<>hYzHA@W_*$xgB5Ck`aY0g6?);}Uh- zM_W|IJVFY0iX)F7=5xWnDt+wb{K~HIbrT}JQOAiF<5tlCH28DjHQh^R50L8mC{gdaL%oThnW6Kr>dJt|Ws0f#;jGDK+nIfSnrX36UP z%Od7QI(v<8u+y@@dlYptxLp*6du$|>aj!(TmKYf4f=bLpKA=zFIeM|nNIHBrywt|M zT9~~bLrdf_)%LT`s8uYv{&E>}UsmCyD)v`ASVJ zMM;WAX*3L;y?cVw!pw1HU-~F^ErJ)}5}Q5B#xdC`5}I8BoTwnElBc>F;3B>M8f+UE%wcmCT`#;#mIu| zht^Y*N$yj};rEfaE3=P8#5GJcXQ(CGF^^8Ov{xTn^s^wHW>wGU2B+IjW0#V#pQNer;9;) zKrJ}CG&YpupOvc_-Y6T^ETQgJNZ>6dtvkSJWx)?t9{bX16gQLu;5#`OB`7clq({{o z5l5gG;6}L=(31)Xu?GPL%S6vk;RfvrGvDjb^(1DpgSp{daMm!~Z>%K{fpUq2k)Tk= z_Fo#|+;6~A+94_z4cPalB_o$#!hVLrE_=oGMF^~!8#^0xiRBFf-?lp!7u{IRq`brEu0ZU$3Nb1SzuR?zqDH1JVo41-dpm~#aKM& zKB6hIb?|o{w$sojk}UBj%TJo#+Cm>;>vUjE%x{TPWsBt61UeuQfgZQ!?HXi5yb)22 zLVox8(A@FE&70La!jBz!f9=$w_^@*2ip*)P(@PbC`;q~ikI60IBlJny7QB17mk$txk1!d|d9=GXz1{>IiS3Q#-|0f0 zKa?vozoL;z7b^$zAgD`@@xy&ieH*Rj2OX8y@03qqmwCk<@`bp=>O$DcCiVB;59lH& z^Us~oTim;Ks9-#C=F|pFAJ4mQuGr|fho&a-pw2~ZuP20Cu+oSh#dIMal9Ra9?kMj} zt|TJt23L;Xzj4yWf_HBHk~l`EOol|KN+$nNW!Q1PbPuDBnlso}PepO{v=k=q(pU7?f8S%1yZ4GHqn-e zJC3Q#hBM+!kvDeyYmHicc0uXBc+Otj7(BfR+tI|uuQfvkE>8#nl_%N&A0PTeNz>^k zaY^N>pmx;u^o6L(Rkq))04BTD*9HaTg4!+DGbhpbTI9dZm%A5S@!%Fdy*+E~1@w(u z>N=R*G~f4th-*)d0~}iXg?Nq!XQk0q@jwkibTy}zwLfe+065@ueYn9wa9Xxm$EG9~ z-^jf}aC`y=3IN!Cp?V{0%w&uMcPwyAPhHu;7U%v%O4-IJCKC0IFVD$iw66aAf}*le zK~noMHPvmf&sa2+6#AKfrns(#x!j`dZ#4g1RuS}k(6({?NA>$mD^ zjNGQHu}dTTNix&7x?}shirM^x0g6&OAKtn52_Kkm(s}fOWMmE+h+c4*fg}y*ur(#G z$Vna&>^t_mk3o^Av$t2hc@F_e8ZU<5scT+oAWCt5&U8kQY;h=)EHtnOVU-n0Y8$3% zs|yFRqO6yT9F>R=-^?^dXIcMTn1)kIr7nBNDfpB-p9u_uLS88yls)h}O|G2Kf`v4( z679vlt|sn^W=;{eODbRhP1#I0rEP(o^F(wr<$AZJQakZ9BuZZQIJQBQk8;w#^JP<3!e~wO6e=Pwlho zKl@@`eD~ke=j>z5(OVmDlh_nTX-JCF56Zk}{fX9~Co0AGCQ`RcrZ2+yhWQh{A$JIj z(Id_zf6gp=r!?sa^`#X7n~%+K|CfbThyK9OIcuH0hRkwRdzI&pGD;+@E8B#c_(JDd z-ds7yho%>cb`)xD?9ut3XEA=fEHC=cap#ReU80(>WWw7NfgmB zE}|Fw)JiG~O9U&w0?(s!ToUG7JU?P_#%(aX+!jw=ylq1iXTC{%8gtpefX6SJHYBq& zW=UcQ3#)Va*X~4;oTctZDvY&15nJ7e;5dWnRp!ym_ng7T<%q2-c5y%pyjVOU4X9gh zU{pp&B8D+i;gc#@r8#~hF~_O~ySZkPzm%mRl3-`pGr}>b26%o@zOXf@W-B#gRzv1B z>`NJ~O2)sC)=Qzg{<7w^wfLsKH9hY7??!ujZ)RgJ-D~h}jeHT|eu~>v%N!Ur56M-4 z^Cs!&GIfGm(<+9lMeLfFFIj%L}Q-?YO|0D#SuY+k|fLhDsAF*s6Ep6uccF60MOKf<+vSn=h}!u!MBm!b4ax zZ&35ZtwUYO>!In*>tWtK`-BG4(lY;cjl)Gaot`May{>SYSgD*-6H|#jdb!?cMbe3H zp&qvdgwJDrGuy5Fax!38O}4Xbhq14_YJ4`q`}|P=d4krxCfiC4FXKcgZo%~|V<*Ku z5$?%6G{7V2AbZJxOzZG~C1j@x_%}LbT;A_}pvG z%j&AnhS^D!wW(G`kh5W5Ffw|W#wwuG0W_nN>78q#BOPX+F0-}$f{98;@dN}>24dr= z^kRlG?_&d>#PmV*>87Q9wt2iDBOlZ`R7V7Jv1O1QuX}J87&?}c=alI(?No~$N1tr8 zCy$LQ0me;>wzf6gQZUP;mKbEFMpb#56JexlgtS!S;dg9nghX0pqdm~5R4Smzr8aNt{cURVBFEM>MZ?&B%MC1!f6I{+hPZY&7<@MWdw9@xpquw|* z6(}Z12Y#~QXNY$8te#LzJOJTk)fXo~+o>J_+04>E?PrvEzZkys8M2XF^|X9%C$Qmb zdpuNAk{mQbM|661<5iCaVJj;yZiQ!qP3wz~2<8P1mh2H&*^J`_ zkA#1;|E{(J#-oO5TRB{FqTLWeioAD)qLStR!*Km@FRyU6SiZEmYI47B!|WXABnmE8 zxbP!h-MEDtp|paTq&b?T*#h<2_?F3I{+@3=3KnRb2O68u^awJFzSZ&J38tjX@2n5O zzif|GRG-eEmO|FtY|6D7=KVyeAbg9YxF3j{k@A}jN|fTbUrX$v6Z1#PDjGYPm9az7 z#6_2bxi_7)Fx)C-U9O;Q!n-X~B6lrNH8O!-cJ^Xq(L=JQi`~lo$v`*%R~FiVz?k*Z z(uU(5;0W-*zM4RkrZwt2If(%0D!g9rAPw9FLzIMQcx|jPGKymBL9bGr=4{H=tI2}( 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%0A}t^h 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 MH { await root.setup(); await expect(root.start()).rejects.toThrow( - /Unable to migrate the corrupt saved object document with _id: 'index-pattern:test_index\*'/ + 'Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: Corrupt saved object documents: index-pattern:test_index*. To allow migrations to proceed, please delete these documents.' ); const logFileContent = await asyncReadFile(logFilePath, 'utf-8'); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts new file mode 100644 index 0000000000000..e48f1e65c120f --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import Fs from 'fs'; +import Util from 'util'; +import * as kbnTestServer from '../../../../test_helpers/kbn_server'; +import { Root } from '../../../root'; + +const logFilePath = Path.join(__dirname, 'migration_test_corrupt_docs_kibana.log'); + +const asyncUnlink = Util.promisify(Fs.unlink); +async function removeLogFile() { + // ignore errors if it doesn't exist + await asyncUnlink(logFilePath).catch(() => void 0); +} + +describe('migration v2 with corrupt saved object documents', () => { + let esServer: kbnTestServer.TestElasticsearchUtils; + let root: Root; + + beforeAll(async () => { + await removeLogFile(); + }); + + afterAll(async () => { + if (root) { + await root.shutdown(); + } + if (esServer) { + await esServer.stop(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + }); + + it('collects corrupt saved object documents accross batches', async () => { + const { startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'basic', + // original uncorrupt SO: + // { + // type: 'foo', // 'bar', 'baz' + // foo: {}, // bar: {}, baz: {} + // migrationVersion: { + // foo: '7.13.0', + // }, + // }, + // original corrupt SO example: + // { + // id: 'bar:123' + // type: 'foo', + // foo: {}, + // migrationVersion: { + // foo: '7.13.0', + // }, + // }, + // contains migrated index with 8.0 aliases to skip migration, but run outdated doc search + dataArchive: Path.join( + __dirname, + 'archives', + '8.0.0_migrated_with_corrupt_outdated_docs.zip' + ), + }, + }, + }); + + root = createRoot(); + + esServer = await startES(); + const coreSetup = await root.setup(); + + coreSetup.savedObjects.registerType({ + name: 'foo', + hidden: false, + mappings: { properties: {} }, + namespaceType: 'agnostic', + migrations: { + '7.14.0': (doc) => doc, + }, + }); + coreSetup.savedObjects.registerType({ + name: 'bar', + hidden: false, + mappings: { properties: {} }, + namespaceType: 'agnostic', + migrations: { + '7.14.0': (doc) => doc, + }, + }); + coreSetup.savedObjects.registerType({ + name: 'baz', + hidden: false, + mappings: { properties: {} }, + namespaceType: 'agnostic', + migrations: { + '7.14.0': (doc) => doc, + }, + }); + try { + await root.start(); + } catch (err) { + const corruptFooSOs = /foo:/g; + const corruptBarSOs = /bar:/g; + const corruptBazSOs = /baz:/g; + expect( + [ + ...err.message.matchAll(corruptFooSOs), + ...err.message.matchAll(corruptBarSOs), + ...err.message.matchAll(corruptBazSOs), + ].length + ).toEqual(16); + } + }); +}); + +function createRoot() { + return kbnTestServer.createRootWithCorePlugins( + { + migrations: { + skip: false, + enableV2: true, + batchSize: 5, + }, + logging: { + appenders: { + file: { + type: 'file', + fileName: logFilePath, + layout: { + type: 'json', + }, + }, + }, + loggers: [ + { + name: 'root', + appenders: ['file'], + }, + ], + }, + }, + { + oss: true, + } + ); +} diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts index 85cc86fe0a468..8443f837a7f1d 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts @@ -10,7 +10,6 @@ import { errors as EsErrors } from '@elastic/elasticsearch'; import * as Option from 'fp-ts/lib/Option'; import { Logger, LogMeta } from '../../logging'; import type { ElasticsearchClient } from '../../elasticsearch'; -import { CorruptSavedObjectError } from '../migrations/core/migrate_raw_docs'; import { Model, Next, stateActionMachine } from './state_action_machine'; import { cleanup } from './migrations_state_machine_cleanup'; import { State } from './types'; @@ -74,7 +73,6 @@ const logActionResponse = ( ) => { logger.debug(logMessagePrefix + `${state.controlState} RESPONSE`, res as LogMeta); }; - const dumpExecutionLog = (logger: Logger, logMessagePrefix: string, executionLog: ExecutionLog) => { logger.error(logMessagePrefix + 'migration failed, dumping execution log:'); executionLog.forEach((log) => { @@ -211,11 +209,6 @@ export async function migrationStateActionMachine({ logger.error(e); dumpExecutionLog(logger, logMessagePrefix, executionLog); - if (e instanceof CorruptSavedObjectError) { - throw new Error( - `${e.message} To allow migrations to proceed, please delete this document from the [${initialState.indexPrefix}_${initialState.kibanaVersion}_001] index.` - ); - } const newError = new Error( `Unable to complete saved object migrations for the [${initialState.indexPrefix}] index. ${e}` diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 213e8b43c0ea0..bdaedba9c9ea3 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -36,12 +36,15 @@ import type { CloneTempToSource, SetTempWriteBlock, WaitForYellowSourceState, + TransformedDocumentsBulkIndex, + ReindexSourceToTempIndexBulk, } from './types'; import { SavedObjectsRawDoc } from '..'; import { AliasAction, RetryableEsClientError } from './actions'; import { createInitialState, model } from './model'; import { ResponseType } from './next'; import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; +import { TransformErrorObjects, TransformSavedObjectDocumentError } from '../migrations/core'; describe('migrations v2 model', () => { const baseState: BaseState = { @@ -778,6 +781,8 @@ describe('migrations v2 model', () => { targetIndex: '.kibana_7.11.0_001', tempIndexMappings: { properties: {} }, lastHitSortValue: undefined, + corruptDocumentIds: [], + transformErrors: [], }; it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_INDEX if the index has outdated documents to reindex', () => { @@ -802,6 +807,23 @@ describe('migrations v2 model', () => { expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT'); expect(newState.sourceIndexPitId).toBe('pit_id'); }); + + it('REINDEX_SOURCE_TO_TEMP_READ -> FATAL if no outdated documents to reindex and transform failures seen with previous outdated documents', () => { + const testState: ReindexSourceToTempRead = { + ...state, + corruptDocumentIds: ['a:b'], + transformErrors: [], + }; + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({ + outdatedDocuments: [], + lastHitSortValue: undefined, + }); + const newState = model(testState, res) as FatalState; + expect(newState.controlState).toBe('FATAL'); + expect(newState.reason).toMatchInlineSnapshot( + `"Migrations failed. Reason: Corrupt saved object documents: a:b. To allow migrations to proceed, please delete these documents."` + ); + }); }); describe('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT', () => { @@ -833,38 +855,89 @@ describe('migrations v2 model', () => { sourceIndexPitId: 'pit_id', targetIndex: '.kibana_7.11.0_001', lastHitSortValue: undefined, + corruptDocumentIds: [], + transformErrors: [], }; + const processedDocs = [ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + ] as SavedObjectsRawDoc[]; - it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right( - 'bulk_index_succeeded' - ); + it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_INDEX_BULK if action succeeded', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right({ + processedDocs, + }); const newState = model(state, res); + expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_INDEX_BULK'); + }); + + it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded but we have carried through previous failures', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right({ + processedDocs, + }); + const testState = { + ...state, + corruptDocumentIds: ['a:b'], + transformErrors: [], + }; + const newState = model(testState, res) as ReindexSourceToTempIndex; expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ'); - expect(newState.retryCount).toEqual(0); - expect(newState.retryDelay).toEqual(0); + expect(newState.corruptDocumentIds.length).toEqual(1); + expect(newState.transformErrors.length).toEqual(0); }); - it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ when response is left target_index_had_write_block', () => { + it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ when response is left documents_transform_failed', () => { const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.left({ - type: 'target_index_had_write_block', + type: 'documents_transform_failed', + corruptDocumentIds: ['a:b'], + transformErrors: [], }); const newState = model(state, res) as ReindexSourceToTempRead; expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ'); + expect(newState.corruptDocumentIds.length).toEqual(1); + expect(newState.transformErrors.length).toEqual(0); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); - - it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ when response is left index_not_found_exception for temp index', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.left({ - type: 'index_not_found_exception', - index: state.tempIndex, - }); - const newState = model(state, res) as ReindexSourceToTempRead; + }); + describe('REINDEX_SOURCE_TO_TEMP_INDEX_BULK', () => { + const transformedDocs = [ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + ] as SavedObjectsRawDoc[]; + const reindexSourceToTempIndexBulkState: ReindexSourceToTempIndexBulk = { + ...baseState, + controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK', + transformedDocs, + versionIndexReadyActions: Option.none, + sourceIndex: Option.some('.kibana') as Option.Some, + sourceIndexPitId: 'pit_id', + targetIndex: '.kibana_7.11.0_001', + lastHitSortValue: undefined, + }; + test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> = Either.right( + 'bulk_index_succeeded' + ); + const newState = model(reindexSourceToTempIndexBulkState, res); expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ'); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); + test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK should throw a throwBadResponse error if action failed', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> = Either.left({ + type: 'retryable_es_client_error', + message: 'random documents bulk index error', + }); + const newState = model(reindexSourceToTempIndexBulkState, res); + expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_INDEX_BULK'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); + }); }); describe('SET_TEMP_WRITE_BLOCK', () => { @@ -943,6 +1016,8 @@ describe('migrations v2 model', () => { targetIndex: '.kibana_7.11.0_001', lastHitSortValue: undefined, hasTransformedDocs: false, + corruptDocumentIds: [], + transformErrors: [], }; it('OUTDATED_DOCUMENTS_SEARCH_READ -> OUTDATED_DOCUMENTS_TRANSFORM if found documents to transform', () => { @@ -967,6 +1042,37 @@ describe('migrations v2 model', () => { expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT'); expect(newState.pitId).toBe('pit_id'); }); + + it('OUTDATED_DOCUMENTS_SEARCH_READ -> FATAL if no outdated documents to transform and we have failed document migrations', () => { + const corruptDocumentIdsCarriedOver = ['a:somethingelse']; + const originalTransformError = new Error('something went wrong'); + const transFormErr = new TransformSavedObjectDocumentError( + '123', + 'vis', + undefined, + 'randomvis: 7.12.0', + 'failedDoc', + originalTransformError + ); + const transformationErrors = [ + { rawId: 'bob:tail', err: transFormErr }, + ] as TransformErrorObjects[]; + const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH_READ'> = Either.right({ + outdatedDocuments: [], + lastHitSortValue: undefined, + }); + const transformErrorsState: OutdatedDocumentsSearchRead = { + ...state, + corruptDocumentIds: [...corruptDocumentIdsCarriedOver], + transformErrors: [...transformationErrors], + }; + const newState = model(transformErrorsState, res) as FatalState; + expect(newState.controlState).toBe('FATAL'); + expect(newState.reason.includes('Migrations failed. Reason:')).toBe(true); + expect(newState.reason.includes('Corrupt saved object documents: ')).toBe(true); + expect(newState.reason.includes('Transformation errors: ')).toBe(true); + expect(newState.reason.includes('randomvis: 7.12.0')).toBe(true); + }); }); describe('OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT', () => { @@ -1006,9 +1112,20 @@ describe('migrations v2 model', () => { }); describe('OUTDATED_DOCUMENTS_TRANSFORM', () => { - const outdatedDocuments = ([ - Symbol('raw saved object doc'), - ] as unknown) as SavedObjectsRawDoc[]; + const outdatedDocuments = [{ _id: '1', _source: { type: 'vis' } }]; + const corruptDocumentIds = ['a:somethingelse']; + const originalTransformError = new Error('Dang diggity!'); + const transFormErr = new TransformSavedObjectDocumentError( + 'id', + 'type', + 'namespace', + 'failedTransform', + 'failedDoc', + originalTransformError + ); + const transformationErrors = [ + { rawId: 'bob:tail', err: transFormErr }, + ] as TransformErrorObjects[]; const outdatedDocumentsTransformState: OutdatedDocumentsTransform = { ...baseState, controlState: 'OUTDATED_DOCUMENTS_TRANSFORM', @@ -1016,18 +1133,132 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana') as Option.Some, targetIndex: '.kibana_7.11.0_001', outdatedDocuments, + corruptDocumentIds: [], + transformErrors: [], pitId: 'pit_id', lastHitSortValue: [3, 4], hasTransformedDocs: false, }; - test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ if action succeeds', () => { - const res: ResponseType<'OUTDATED_DOCUMENTS_TRANSFORM'> = Either.right( - 'bulk_index_succeeded' - ); - const newState = model(outdatedDocumentsTransformState, res); - expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_READ'); - expect(newState.retryCount).toEqual(0); - expect(newState.retryDelay).toEqual(0); + describe('OUTDATED_DOCUMENTS_TRANSFORM if action succeeds', () => { + const processedDocs = [ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + ] as SavedObjectsRawDoc[]; + test('OUTDATED_DOCUMENTS_TRANSFORM -> TRANSFORMED_DOCUMENTS_BULK_INDEX if action succeeds', () => { + const res: ResponseType<'OUTDATED_DOCUMENTS_TRANSFORM'> = Either.right({ processedDocs }); + const newState = model( + outdatedDocumentsTransformState, + res + ) as TransformedDocumentsBulkIndex; + expect(newState.controlState).toEqual('TRANSFORMED_DOCUMENTS_BULK_INDEX'); + expect(newState.transformedDocs).toEqual(processedDocs); + expect(newState.retryCount).toEqual(0); + expect(newState.retryDelay).toEqual(0); + }); + test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ if there are are existing documents that failed transformation', () => { + const outdatedDocumentsTransformStateWithFailedDocuments: OutdatedDocumentsTransform = { + ...outdatedDocumentsTransformState, + corruptDocumentIds: [...corruptDocumentIds], + transformErrors: [], + }; + const res: ResponseType<'OUTDATED_DOCUMENTS_TRANSFORM'> = Either.right({ processedDocs }); + const newState = model( + outdatedDocumentsTransformStateWithFailedDocuments, + res + ) as OutdatedDocumentsSearchRead; + expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_READ'); + expect(newState.corruptDocumentIds).toEqual(corruptDocumentIds); + expect(newState.retryCount).toEqual(0); + expect(newState.retryDelay).toEqual(0); + }); + test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ if there are are existing documents that failed transformation because of transform errors', () => { + const outdatedDocumentsTransformStateWithFailedDocuments: OutdatedDocumentsTransform = { + ...outdatedDocumentsTransformState, + corruptDocumentIds: [], + transformErrors: [...transformationErrors], + }; + const res: ResponseType<'OUTDATED_DOCUMENTS_TRANSFORM'> = Either.right({ processedDocs }); + const newState = model( + outdatedDocumentsTransformStateWithFailedDocuments, + res + ) as OutdatedDocumentsSearchRead; + expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_READ'); + expect(newState.corruptDocumentIds.length).toEqual(0); + expect(newState.transformErrors.length).toEqual(1); + expect(newState.retryCount).toEqual(0); + expect(newState.retryDelay).toEqual(0); + }); + }); + describe('OUTDATED_DOCUMENTS_TRANSFORM if action fails', () => { + test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ adding newly failed documents to state if documents failed the transform', () => { + const res: ResponseType<'OUTDATED_DOCUMENTS_TRANSFORM'> = Either.left({ + type: 'documents_transform_failed', + corruptDocumentIds, + transformErrors: [], + }); + const newState = model( + outdatedDocumentsTransformState, + res + ) as OutdatedDocumentsSearchRead; + expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_READ'); + expect(newState.corruptDocumentIds).toEqual(corruptDocumentIds); + }); + test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ combines newly failed documents with those already on state if documents failed the transform', () => { + const newFailedTransformDocumentIds = ['b:other', 'c:__']; + const outdatedDocumentsTransformStateWithFailedDocuments: OutdatedDocumentsTransform = { + ...outdatedDocumentsTransformState, + corruptDocumentIds: [...corruptDocumentIds], + transformErrors: [...transformationErrors], + }; + const res: ResponseType<'OUTDATED_DOCUMENTS_TRANSFORM'> = Either.left({ + type: 'documents_transform_failed', + corruptDocumentIds: newFailedTransformDocumentIds, + transformErrors: transformationErrors, + }); + const newState = model( + outdatedDocumentsTransformStateWithFailedDocuments, + res + ) as OutdatedDocumentsSearchRead; + expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_READ'); + expect(newState.corruptDocumentIds).toEqual([ + ...corruptDocumentIds, + ...newFailedTransformDocumentIds, + ]); + }); + }); + }); + describe('TRANSFORMED_DOCUMENTS_BULK_INDEX', () => { + const transformedDocs = [ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + ] as SavedObjectsRawDoc[]; + const transformedDocumentsBulkIndexState: TransformedDocumentsBulkIndex = { + ...baseState, + controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX', + transformedDocs, + versionIndexReadyActions: Option.none, + sourceIndex: Option.some('.kibana') as Option.Some, + targetIndex: '.kibana_7.11.0_001', + pitId: 'pit_id', + lastHitSortValue: [3, 4], + hasTransformedDocs: false, + }; + test('TRANSFORMED_DOCUMENTS_BULK_INDEX should throw a throwBadResponse error if action failed', () => { + const res: ResponseType<'TRANSFORMED_DOCUMENTS_BULK_INDEX'> = Either.left({ + type: 'retryable_es_client_error', + message: 'random documents bulk index error', + }); + const newState = model( + transformedDocumentsBulkIndexState, + res + ) as TransformedDocumentsBulkIndex; + expect(newState.controlState).toEqual('TRANSFORMED_DOCUMENTS_BULK_INDEX'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 318eff19d5e24..cf9d6aec6b5b0 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -16,7 +16,7 @@ import { IndexMapping } from '../mappings'; import { ResponseType } from './next'; import { SavedObjectsMigrationVersion } from '../types'; import { disableUnknownTypeMappingFields } from '../migrations/core/migration_context'; -import { excludeUnusedTypesQuery } from '../migrations/core'; +import { excludeUnusedTypesQuery, TransformErrorObjects } from '../migrations/core'; import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; /** @@ -97,6 +97,31 @@ function getAliases(indices: FetchIndexResponse) { }, {} as Record); } +/** + * Constructs migration failure message strings from corrupt document ids and document transformation errors + */ +function extractTransformFailuresReason( + corruptDocumentIds: string[], + transformErrors: TransformErrorObjects[] +): { corruptDocsReason: string; transformErrsReason: string } { + const corruptDocumentIdReason = + corruptDocumentIds.length > 0 + ? ` Corrupt saved object documents: ${corruptDocumentIds.join(',')}` + : ''; + // we have both the saved object Id and the stack trace in each `transformErrors` item. + const transformErrorsReason = + transformErrors.length > 0 + ? ' Transformation errors: ' + + transformErrors + .map((errObj) => `${errObj.rawId}: ${errObj.err.message}\n ${errObj.err.stack ?? ''}`) + .join('/n') + : ''; + return { + corruptDocsReason: corruptDocumentIdReason, + transformErrsReason: transformErrorsReason, + }; +} + const delayRetryState = ( state: S, errorMessage: string, @@ -481,11 +506,15 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'REINDEX_SOURCE_TO_TEMP_READ', sourceIndexPitId: res.right.pitId, lastHitSortValue: undefined, + // placeholders to collect document transform problems + corruptDocumentIds: [], + transformErrors: [], }; } else { throwBadResponse(stateP, res); } } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_READ') { + // we carry through any failures we've seen with transforming documents on state const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { if (res.right.outdatedDocuments.length > 0) { @@ -495,11 +524,27 @@ export const model = (currentState: State, resW: ResponseType): outdatedDocuments: res.right.outdatedDocuments, lastHitSortValue: res.right.lastHitSortValue, }; + } else { + // we don't have any more outdated documents and need to either fail or move on to updating the target mappings. + if (stateP.corruptDocumentIds.length > 0 || stateP.transformErrors.length > 0) { + const { corruptDocsReason, transformErrsReason } = extractTransformFailuresReason( + stateP.corruptDocumentIds, + stateP.transformErrors + ); + return { + ...stateP, + controlState: 'FATAL', + reason: `Migrations failed. Reason:${corruptDocsReason}${transformErrsReason}. To allow migrations to proceed, please delete these documents.`, + }; + } else { + // we don't have any more outdated documents and we haven't encountered any document transformation issues. + // Close the PIT search and carry on with the happy path. + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT', + }; + } } - return { - ...stateP, - controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT', - }; } else { throwBadResponse(stateP, res); } @@ -516,34 +561,55 @@ export const model = (currentState: State, resW: ResponseType): throwBadResponse(stateP, res); } } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_INDEX') { + // We follow a similar control flow as for + // outdated document search -> outdated document transform -> transform documents bulk index + // collecting issues along the way rather than failing + // REINDEX_SOURCE_TO_TEMP_INDEX handles the document transforms const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { - return { - ...stateP, - controlState: 'REINDEX_SOURCE_TO_TEMP_READ', - }; + if (stateP.corruptDocumentIds.length === 0 && stateP.transformErrors.length === 0) { + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK', // handles the actual bulk indexing into temp index + transformedDocs: [...res.right.processedDocs], + }; + } else { + // we don't have any transform issues with the current batch of outdated docs but + // we have carried through previous transformation issues. + // The migration will ultimately fail but before we do that, continue to + // search through remaining docs for more issues and pass the previous failures along on state + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_READ', + }; + } } else { + // we have failures from the current batch of documents and add them to the lists const left = res.left; - if ( - isLeftTypeof(left, 'target_index_had_write_block') || - (isLeftTypeof(left, 'index_not_found_exception') && left.index === stateP.tempIndex) - ) { - // index_not_found_exception: - // another instance completed the MARK_VERSION_INDEX_READY and - // removed the temp index. - // target_index_had_write_block - // another instance completed the SET_TEMP_WRITE_BLOCK step adding a - // write block to the temp index. - // - // For simplicity we continue linearly through the next steps even if - // we know another instance already completed these. + if (isLeftTypeof(left, 'documents_transform_failed')) { return { ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP_READ', + corruptDocumentIds: [...stateP.corruptDocumentIds, ...left.corruptDocumentIds], + transformErrors: [...stateP.transformErrors, ...left.transformErrors], }; + } else { + // should never happen + throwBadResponse(stateP, res as never); } - // should never happen - throwBadResponse(stateP, res as never); + } + } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK') { + const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_READ', + // we're still on the happy path with no transformation failures seen. + corruptDocumentIds: [], + transformErrors: [], + }; + } else { + throwBadResponse(stateP, res); } } else if (stateP.controlState === 'SET_TEMP_WRITE_BLOCK') { const res = resW as ExcludeRetryableEsError>; @@ -611,6 +677,8 @@ export const model = (currentState: State, resW: ResponseType): pitId: res.right.pitId, lastHitSortValue: undefined, hasTransformedDocs: false, + corruptDocumentIds: [], + transformErrors: [], }; } else { throwBadResponse(stateP, res); @@ -626,59 +694,111 @@ export const model = (currentState: State, resW: ResponseType): lastHitSortValue: res.right.lastHitSortValue, }; } else { + // we don't have any more outdated documents and need to either fail or move on to updating the target mappings. + if (stateP.corruptDocumentIds.length > 0 || stateP.transformErrors.length > 0) { + const { corruptDocsReason, transformErrsReason } = extractTransformFailuresReason( + stateP.corruptDocumentIds, + stateP.transformErrors + ); + return { + ...stateP, + controlState: 'FATAL', + reason: `Migrations failed. Reason:${corruptDocsReason}${transformErrsReason}. To allow migrations to proceed, please delete these documents.`, + }; + } else { + // If there are no more results we have transformed all outdated + // documents and we didn't encounter any corrupt documents or transformation errors + // and can proceed to the next step + return { + ...stateP, + controlState: 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT', + }; + } + } + } else { + throwBadResponse(stateP, res); + } + } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_TRANSFORM') { + const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { + // we haven't seen corrupt documents or any transformation errors thus far in the migration + // index the migrated docs + if (stateP.corruptDocumentIds.length === 0 && stateP.transformErrors.length === 0) { + return { + ...stateP, + controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX', + transformedDocs: [...res.right.processedDocs], + hasTransformedDocs: true, + }; + } else { + // We have seen corrupt documents and/or transformation errors + // skip indexing and go straight to reading and transforming more docs return { ...stateP, - controlState: 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT', + controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', }; } } else { - throwBadResponse(stateP, res); + if (isLeftTypeof(res.left, 'documents_transform_failed')) { + // continue to build up any more transformation errors before failing the migration. + return { + ...stateP, + controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', + corruptDocumentIds: [...stateP.corruptDocumentIds, ...res.left.corruptDocumentIds], + transformErrors: [...stateP.transformErrors, ...res.left.transformErrors], + hasTransformedDocs: false, + }; + } else { + throwBadResponse(stateP, res as never); + } } - } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_REFRESH') { + } else if (stateP.controlState === 'TRANSFORMED_DOCUMENTS_BULK_INDEX') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { return { ...stateP, - controlState: 'UPDATE_TARGET_MAPPINGS', + controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', + corruptDocumentIds: [], + transformErrors: [], + hasTransformedDocs: true, }; } else { throwBadResponse(stateP, res); } - } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT') { + } else if (stateP.controlState === 'UPDATE_TARGET_MAPPINGS') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { - const { pitId, hasTransformedDocs, ...state } = stateP; - if (hasTransformedDocs) { - return { - ...state, - controlState: 'OUTDATED_DOCUMENTS_REFRESH', - }; - } return { - ...state, - controlState: 'UPDATE_TARGET_MAPPINGS', + ...stateP, + controlState: 'UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK', + updateTargetMappingsTaskId: res.right.taskId, }; } else { - throwBadResponse(stateP, res); + throwBadResponse(stateP, res as never); } - } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_TRANSFORM') { + } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_REFRESH') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { return { ...stateP, - controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', - hasTransformedDocs: true, + controlState: 'UPDATE_TARGET_MAPPINGS', }; } else { - throwBadResponse(stateP, res as never); + throwBadResponse(stateP, res); } - } else if (stateP.controlState === 'UPDATE_TARGET_MAPPINGS') { + } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { + const { pitId, hasTransformedDocs, ...state } = stateP; + if (hasTransformedDocs) { + return { + ...state, + controlState: 'OUTDATED_DOCUMENTS_REFRESH', + }; + } return { - ...stateP, - controlState: 'UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK', - updateTargetMappingsTaskId: res.right.taskId, + ...state, + controlState: 'UPDATE_TARGET_MAPPINGS', }; } else { throwBadResponse(stateP, res); diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index 536c07d6a071d..07ebf80271d48 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -32,6 +32,8 @@ import type { SetTempWriteBlock, WaitForYellowSourceState, TransformRawDocs, + TransformedDocumentsBulkIndex, + ReindexSourceToTempIndexBulk, OutdatedDocumentsSearchOpenPit, OutdatedDocumentsSearchRead, OutdatedDocumentsSearchClosePit, @@ -82,11 +84,12 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra REINDEX_SOURCE_TO_TEMP_CLOSE_PIT: (state: ReindexSourceToTempClosePit) => Actions.closePit(client, state.sourceIndexPitId), REINDEX_SOURCE_TO_TEMP_INDEX: (state: ReindexSourceToTempIndex) => - Actions.transformDocs( + Actions.transformDocs(transformRawDocs, state.outdatedDocuments), + REINDEX_SOURCE_TO_TEMP_INDEX_BULK: (state: ReindexSourceToTempIndexBulk) => + Actions.bulkOverwriteTransformedDocuments( client, - transformRawDocs, - state.outdatedDocuments, state.tempIndex, + state.transformedDocs, /** * Since we don't run a search against the target index, we disable "refresh" to speed up * the migration process. @@ -121,11 +124,12 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra OUTDATED_DOCUMENTS_REFRESH: (state: OutdatedDocumentsRefresh) => Actions.refreshIndex(client, state.targetIndex), OUTDATED_DOCUMENTS_TRANSFORM: (state: OutdatedDocumentsTransform) => - Actions.transformDocs( + Actions.transformDocs(transformRawDocs, state.outdatedDocuments), + TRANSFORMED_DOCUMENTS_BULK_INDEX: (state: TransformedDocumentsBulkIndex) => + Actions.bulkOverwriteTransformedDocuments( client, - transformRawDocs, - state.outdatedDocuments, state.targetIndex, + state.transformedDocs, /** * Since we don't run a search against the target index, we disable "refresh" to speed up * the migration process. diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index ac807e9d61776..f5800a3cd9570 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -6,12 +6,18 @@ * Side Public License, v 1. */ +import * as TaskEither from 'fp-ts/lib/TaskEither'; import * as Option from 'fp-ts/lib/Option'; import { estypes } from '@elastic/elasticsearch'; import { ControlState } from './state_action_machine'; import { AliasAction } from './actions'; import { IndexMapping } from '../mappings'; import { SavedObjectsRawDoc } from '..'; +import { TransformErrorObjects } from '../migrations/core'; +import { + DocumentsTransformFailed, + DocumentsTransformSuccess, +} from '../migrations/core/migrate_raw_docs'; export type MigrationLogLevel = 'error' | 'info'; @@ -175,6 +181,8 @@ export interface ReindexSourceToTempRead extends PostInitState { readonly controlState: 'REINDEX_SOURCE_TO_TEMP_READ'; readonly sourceIndexPitId: string; readonly lastHitSortValue: number[] | undefined; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; } export interface ReindexSourceToTempClosePit extends PostInitState { @@ -187,6 +195,15 @@ export interface ReindexSourceToTempIndex extends PostInitState { readonly outdatedDocuments: SavedObjectsRawDoc[]; readonly sourceIndexPitId: string; readonly lastHitSortValue: number[] | undefined; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; +} + +export interface ReindexSourceToTempIndexBulk extends PostInitState { + readonly controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'; + readonly transformedDocs: SavedObjectsRawDoc[]; + readonly sourceIndexPitId: string; + readonly lastHitSortValue: number[] | undefined; } export type SetTempWriteBlock = PostInitState & { @@ -233,6 +250,8 @@ export interface OutdatedDocumentsSearchRead extends PostInitState { readonly pitId: string; readonly lastHitSortValue: number[] | undefined; readonly hasTransformedDocs: boolean; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; } export interface OutdatedDocumentsSearchClosePit extends PostInitState { @@ -249,12 +268,24 @@ export interface OutdatedDocumentsRefresh extends PostInitState { } export interface OutdatedDocumentsTransform extends PostInitState { - /** Transform a batch of outdated documents to their latest version and write them to the target index */ + /** Transform a batch of outdated documents to their latest version*/ readonly controlState: 'OUTDATED_DOCUMENTS_TRANSFORM'; readonly pitId: string; readonly outdatedDocuments: SavedObjectsRawDoc[]; readonly lastHitSortValue: number[] | undefined; readonly hasTransformedDocs: boolean; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; +} +export interface TransformedDocumentsBulkIndex extends PostInitState { + /** + * Write the up-to-date transformed documents to the target index + */ + readonly controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX'; + readonly transformedDocs: SavedObjectsRawDoc[]; + readonly lastHitSortValue: number[] | undefined; + readonly hasTransformedDocs: boolean; + readonly pitId: string; } export interface MarkVersionIndexReady extends PostInitState { @@ -351,6 +382,7 @@ export type State = | ReindexSourceToTempRead | ReindexSourceToTempClosePit | ReindexSourceToTempIndex + | ReindexSourceToTempIndexBulk | SetTempWriteBlock | CloneTempToSource | UpdateTargetMappingsState @@ -363,6 +395,7 @@ export type State = | OutdatedDocumentsRefresh | MarkVersionIndexReady | MarkVersionIndexReadyConflict + | TransformedDocumentsBulkIndex | LegacyCreateReindexTargetState | LegacySetWriteBlockState | LegacyReindexState @@ -376,4 +409,6 @@ export type AllControlStates = State['controlState']; */ export type AllActionStates = Exclude; -export type TransformRawDocs = (rawDocs: SavedObjectsRawDoc[]) => Promise; +export type TransformRawDocs = ( + rawDocs: SavedObjectsRawDoc[] +) => TaskEither.TaskEither; From e0c57e08fd13adf297c78da745ac1976289292d1 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Mon, 10 May 2021 16:27:35 -0500 Subject: [PATCH 26/69] [labs] Fix reset overreach; add method for testing project (#99672) --- src/plugins/presentation_util/common/labs.ts | 5 ++-- .../public/services/kibana/labs.ts | 23 ++++++++++++++----- .../presentation_util/public/services/labs.ts | 1 + .../public/services/storybook/labs.ts | 4 ++++ .../public/services/stub/labs.ts | 2 ++ .../canvas/public/services/expressions.ts | 3 +-- .../canvas/public/services/stubs/labs.ts | 1 + 7 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/plugins/presentation_util/common/labs.ts b/src/plugins/presentation_util/common/labs.ts index d551b733ecb8a..902c22681e55e 100644 --- a/src/plugins/presentation_util/common/labs.ts +++ b/src/plugins/presentation_util/common/labs.ts @@ -8,8 +8,9 @@ import { i18n } from '@kbn/i18n'; -export const USE_DATA_SERVICE = 'labs:canvas:useDataService'; -export const TIME_TO_PRESENT = 'labs:presentation:timeToPresent'; +export const LABS_PROJECT_PREFIX = 'labs:'; +export const USE_DATA_SERVICE = `${LABS_PROJECT_PREFIX}canvas:useDataService` as const; +export const TIME_TO_PRESENT = `${LABS_PROJECT_PREFIX}presentation:timeToPresent` as const; export const projectIDs = [TIME_TO_PRESENT, USE_DATA_SERVICE] as const; export const environmentNames = ['kibana', 'browser', 'session'] as const; diff --git a/src/plugins/presentation_util/public/services/kibana/labs.ts b/src/plugins/presentation_util/public/services/kibana/labs.ts index db78103469880..fe0767ff09d8f 100644 --- a/src/plugins/presentation_util/public/services/kibana/labs.ts +++ b/src/plugins/presentation_util/public/services/kibana/labs.ts @@ -7,7 +7,6 @@ */ import { - environmentNames, EnvironmentName, projectIDs, projects, @@ -15,6 +14,7 @@ import { Project, getProjectIDs, SolutionName, + LABS_PROJECT_PREFIX, } from '../../../common'; import { PresentationUtilPluginStartDeps } from '../../types'; import { KibanaPluginServiceFactory } from '../create'; @@ -31,6 +31,16 @@ export type LabsServiceFactory = KibanaPluginServiceFactory< PresentationUtilPluginStartDeps >; +const clearLabsFromStorage = (storage: Storage) => { + projectIDs.forEach((projectID) => storage.removeItem(projectID)); + + // This is a redundancy, to catch any labs that may have been removed above. + // We could consider gathering telemetry to see how often this happens, or this may be unnecessary. + Object.keys(storage) + .filter((key) => key.startsWith(LABS_PROJECT_PREFIX)) + .forEach((key) => storage.removeItem(key)); +}; + export const labsServiceFactory: LabsServiceFactory = ({ coreStart }) => { const { uiSettings } = coreStart; const localStorage = window.localStorage; @@ -75,17 +85,18 @@ export const labsServiceFactory: LabsServiceFactory = ({ coreStart }) => { }; const reset = () => { - localStorage.clear(); - sessionStorage.clear(); - environmentNames.forEach((env) => - projectIDs.forEach((id) => setProjectStatus(id, env, projects[id].isActive)) - ); + clearLabsFromStorage(localStorage); + clearLabsFromStorage(sessionStorage); + projectIDs.forEach((id) => setProjectStatus(id, 'kibana', projects[id].isActive)); }; + const isProjectEnabled = (id: ProjectID) => getProject(id).status.isEnabled; + return { getProjectIDs, getProjects, getProject, + isProjectEnabled, reset, setProjectStatus, }; diff --git a/src/plugins/presentation_util/public/services/labs.ts b/src/plugins/presentation_util/public/services/labs.ts index ef583bd4189a9..70c40eaafa2ef 100644 --- a/src/plugins/presentation_util/public/services/labs.ts +++ b/src/plugins/presentation_util/public/services/labs.ts @@ -20,6 +20,7 @@ import { } from '../../common'; export interface PresentationLabsService { + isProjectEnabled: (id: ProjectID) => boolean; getProjectIDs: () => typeof projectIDs; getProject: (id: ProjectID) => Project; getProjects: (solutions?: SolutionName[]) => Record; diff --git a/src/plugins/presentation_util/public/services/storybook/labs.ts b/src/plugins/presentation_util/public/services/storybook/labs.ts index 396db52460053..8bc526987d95f 100644 --- a/src/plugins/presentation_util/public/services/storybook/labs.ts +++ b/src/plugins/presentation_util/public/services/storybook/labs.ts @@ -46,13 +46,17 @@ export const labsServiceFactory: LabsServiceFactory = () => { }; const reset = () => { + // This is normally not ok, but it's our isolated Storybook instance. storage.clear(); }; + const isProjectEnabled = (id: ProjectID) => getProject(id).status.isEnabled; + return { getProjectIDs, getProjects, getProject, + isProjectEnabled, reset, setProjectStatus, }; diff --git a/src/plugins/presentation_util/public/services/stub/labs.ts b/src/plugins/presentation_util/public/services/stub/labs.ts index c511ed26ef32e..aee7ce20bd86a 100644 --- a/src/plugins/presentation_util/public/services/stub/labs.ts +++ b/src/plugins/presentation_util/public/services/stub/labs.ts @@ -64,11 +64,13 @@ export const labsServiceFactory: LabsServiceFactory = () => { const setProjectStatus = (id: ProjectID, env: EnvironmentName, value: boolean) => { statuses[id] = { ...statuses[id], [env]: value }; }; + const isProjectEnabled = (id: ProjectID) => getProject(id).status.isEnabled; return { getProjectIDs, getProject, getProjects, + isProjectEnabled, setProjectStatus, reset: () => { statuses = reset(); diff --git a/x-pack/plugins/canvas/public/services/expressions.ts b/x-pack/plugins/canvas/public/services/expressions.ts index fd733862c4b67..35493341e0e88 100644 --- a/x-pack/plugins/canvas/public/services/expressions.ts +++ b/x-pack/plugins/canvas/public/services/expressions.ts @@ -25,8 +25,7 @@ export const expressionsServiceFactory: CanvasServiceFactory if (!cached) { cached = (async () => { const labService = startPlugins.presentationUtil.labsService; - const useDataSearchProject = labService.getProject('labs:canvas:useDataService'); - const hasDataSearch = useDataSearchProject.status.isEnabled; + const hasDataSearch = labService.isProjectEnabled('labs:canvas:useDataService'); const dataSearchFns = ['essql', 'esdocs', 'escount']; const serverFunctionList = await coreSetup.http.get(API_ROUTE_FUNCTIONS); diff --git a/x-pack/plugins/canvas/public/services/stubs/labs.ts b/x-pack/plugins/canvas/public/services/stubs/labs.ts index 7caa1d0139a70..db89c5c35d5fb 100644 --- a/x-pack/plugins/canvas/public/services/stubs/labs.ts +++ b/x-pack/plugins/canvas/public/services/stubs/labs.ts @@ -14,6 +14,7 @@ export const labsService: CanvasLabsService = { getProject: noop, getProjects: noop, getProjectIDs: () => projectIDs, + isProjectEnabled: () => false, isLabsEnabled: () => true, projectIDs, reset: noop, From 6d269c5062b9dbc9d692265db56a85fa2d5431e1 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Mon, 10 May 2021 17:42:39 -0400 Subject: [PATCH 27/69] Revert "[SecuritySolution] Get endpoint metadata (#99452)" (#99719) This reverts commit 5893d67b4b34c5fdc9446d5e63cd908d777c8b3b. --- .../common/endpoint/types/index.ts | 5 - .../security_solution/hosts/common/index.ts | 9 - .../ml/criteria/host_to_criteria.ts | 3 - .../hosts/containers/hosts/details/index.tsx | 8 +- .../public/hosts/pages/details/index.tsx | 10 +- .../pages/endpoint_hosts/view/index.tsx | 2 +- .../components/host_overview/index.tsx | 13 +- .../server/endpoint/mocks.ts | 10 +- .../endpoint/routes/metadata/handlers.ts | 163 +++++------------- .../endpoint/routes/metadata/metadata.test.ts | 87 ++++------ .../routes/metadata/metadata_v1.test.ts | 78 ++++----- .../routes/metadata/query_builders.test.ts | 5 +- .../routes/metadata/query_builders.ts | 7 +- .../routes/metadata/query_builders_v1.test.ts | 3 +- .../metadata/support/query_strategies.ts | 20 ++- .../endpoint/routes/policy/handlers.test.ts | 24 +-- .../server/endpoint/routes/policy/handlers.ts | 2 +- .../endpoint/routes/policy/service.test.ts | 8 +- .../server/endpoint/routes/policy/service.ts | 30 ++-- .../server/endpoint/types.ts | 3 +- .../security_solution/server/plugin.ts | 5 +- .../factory/hosts/authentications/index.tsx | 1 + .../factory/hosts/details/__mocks__/index.ts | 28 --- .../factory/hosts/details/helpers.ts | 66 +------ .../factory/hosts/details/index.test.tsx | 28 +-- .../factory/hosts/details/index.ts | 40 +---- .../hosts/details/query.host_details.dsl.ts | 19 +- .../security_solution/factory/types.ts | 12 +- .../security_solution/index.ts | 12 +- 29 files changed, 185 insertions(+), 516 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index b9e72bcd625ec..c58e67b5d4fd4 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -414,11 +414,6 @@ export type PolicyInfo = Immutable<{ id: string; }>; -export interface HostMetaDataInfo { - metadata: HostMetadata; - query_strategy_version: MetadataQueryStrategyVersions; -} - export type HostInfo = Immutable<{ metadata: HostMetadata; host_status: HostStatus; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts index 3175876a8299c..a579d8f8d8ef3 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts @@ -25,16 +25,10 @@ export interface EndpointFields { endpointPolicy?: Maybe; sensorVersion?: Maybe; policyStatus?: Maybe; - id?: Maybe; -} - -interface AgentFields { - id?: Maybe; } export interface HostItem { _id?: Maybe; - agent?: Maybe; cloud?: Maybe; endpoint?: Maybe; host?: Maybe; @@ -76,9 +70,6 @@ export interface HostAggEsItem { cloud_machine_type?: HostBuckets; cloud_provider?: HostBuckets; cloud_region?: HostBuckets; - endpoint?: { - id: HostBuckets; - }; host_architecture?: HostBuckets; host_id?: HostBuckets; host_ip?: HostBuckets; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.ts b/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.ts index ff454da7b1fcd..19eae99757849 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.ts @@ -9,9 +9,6 @@ import { HostItem } from '../../../../../common/search_strategy/security_solutio import { CriteriaFields } from '../types'; export const hostToCriteria = (hostItem: HostItem): CriteriaFields[] => { - if (hostItem == null) { - return []; - } if (hostItem.host != null && hostItem.host.name != null) { const criteria: CriteriaFields[] = [ { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx index a0f4386be59a4..dd55bdb4c6948 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx @@ -145,14 +145,14 @@ export const useHostDetails = ({ } return prevRequest; }); - }, [endDate, hostName, indexNames, startDate]); - - useEffect(() => { - hostDetailsSearch(hostDetailsRequest); return () => { searchSubscription$.current.unsubscribe(); abortCtrl.current.abort(); }; + }, [endDate, hostName, indexNames, startDate]); + + useEffect(() => { + hostDetailsSearch(hostDetailsRequest); }, [hostDetailsRequest, hostDetailsSearch]); return [loading, hostDetailsResponse]; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index d88e4f048f917..1ff4abb78b210 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -50,9 +50,6 @@ import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { useHostDetails } from '../../containers/hosts/details'; -import { manageQuery } from '../../../common/components/page/manage_query'; - -const HostOverviewManage = manageQuery(HostOverview); const HostDetailsComponent: React.FC = ({ detailName, hostDetailsPagePath }) => { const dispatch = useDispatch(); @@ -96,12 +93,11 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta ); const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); - const [loading, { hostDetails: hostOverview, id, refetch }] = useHostDetails({ + const [loading, { hostDetails: hostOverview, id }] = useHostDetails({ endDate: to, startDate: from, hostName: detailName, indexNames: selectedPatterns, - skip: selectedPatterns.length === 0, }); const filterQuery = convertToBuildEsQuery({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), @@ -145,7 +141,7 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta skip={isInitializing} > {({ isLoadingAnomaliesData, anomaliesData }) => ( - = ({ detailName, hostDeta to: fromTo.to, }); }} - setQuery={setQuery} - refetch={refetch} /> )} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index f654efdd89ce1..d28bf6b38fd31 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -321,7 +321,7 @@ export const EndpointList = () => { render: (hostStatus: HostInfo['host_status']) => { return ( diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index fa644d1cbcdac..c5d51a9466235 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -86,15 +86,14 @@ export const HostOverview = React.memo( () => [ { title: i18n.HOST_ID, - description: - data && data.host - ? hostIdRenderer({ host: data.host, noLink: true }) - : getEmptyTagValue(), + description: data.host + ? hostIdRenderer({ host: data.host, noLink: true }) + : getEmptyTagValue(), }, { title: i18n.FIRST_SEEN, description: - data && data.host != null && data.host.name && data.host.name.length ? ( + data.host != null && data.host.name && data.host.name.length ? ( ( { title: i18n.LAST_SEEN, description: - data && data.host != null && data.host.name && data.host.name.length ? ( + data.host != null && data.host.name && data.host.name.length ? ( ( )} - {data && data.endpoint != null ? ( + {data.endpoint != null ? ( <> diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 23ea6cc29c3d2..40d4b1a877b2b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { loggingSystemMock, savedObjectsServiceMock } from '../../../../../src/core/server/mocks'; -import { IScopedClusterClient, SavedObjectsClientContract } from '../../../../../src/core/server'; +import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; +import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; import { listMock } from '../../../lists/server/mocks'; import { securityMock } from '../../../security/server/mocks'; import { alertsMock } from '../../../alerting/server/mocks'; @@ -131,11 +131,11 @@ export const createMockMetadataRequestContext = (): jest.Mocked, + dataClient: jest.Mocked, savedObjectsClient: jest.Mocked ) { - const context = (xpackMocks.createRequestHandlerContext() as unknown) as jest.Mocked; - context.core.elasticsearch.client = dataClient; + const context = xpackMocks.createRequestHandlerContext(); + context.core.elasticsearch.legacy.client = dataClient; context.core.savedObjects.client = savedObjectsClient; return context; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 104383f398646..0d59ff2f4ed7b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -6,18 +6,11 @@ */ import Boom from '@hapi/boom'; - +import type { Logger, RequestHandler } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; -import { - IScopedClusterClient, - Logger, - RequestHandler, - SavedObjectsClientContract, -} from '../../../../../../../src/core/server'; import { HostInfo, HostMetadata, - HostMetaDataInfo, HostResultList, HostStatus, MetadataQueryStrategyVersions, @@ -34,11 +27,9 @@ import { findAgentIDsByStatus } from './support/agent_status'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; export interface MetadataRequestContext { - esClient?: IScopedClusterClient; endpointAppContextService: EndpointAppContextService; logger: Logger; - requestHandlerContext?: SecuritySolutionRequestHandlerContext; - savedObjectsClient?: SavedObjectsClientContract; + requestHandlerContext: SecuritySolutionRequestHandlerContext; } const HOST_STATUS_MAPPING = new Map([ @@ -84,11 +75,9 @@ export const getMetadataListRequestHandler = function ( } const metadataRequestContext: MetadataRequestContext = { - esClient: context.core.elasticsearch.client, endpointAppContextService: endpointAppContext.service, logger, requestHandlerContext: context, - savedObjectsClient: context.core.savedObjects.client, }; const unenrolledAgentIds = await findAllUnenrolledAgentIds( @@ -121,10 +110,9 @@ export const getMetadataListRequestHandler = function ( } ); - const result = await context.core.elasticsearch.client.asCurrentUser.search( - queryParams + const hostListQueryResult = queryStrategy!.queryResponseToHostListResult( + await context.core.elasticsearch.legacy.client.callAsCurrentUser('search', queryParams) ); - const hostListQueryResult = queryStrategy!.queryResponseToHostListResult(result.body); return response.ok({ body: await mapToHostResultList(queryParams, hostListQueryResult, metadataRequestContext), }); @@ -148,11 +136,9 @@ export const getMetadataRequestHandler = function ( } const metadataRequestContext: MetadataRequestContext = { - esClient: context.core.elasticsearch.client, endpointAppContextService: endpointAppContext.service, logger, requestHandlerContext: context, - savedObjectsClient: context.core.savedObjects.client, }; try { @@ -178,86 +164,42 @@ export const getMetadataRequestHandler = function ( }; }; -export async function getHostMetaData( +export async function getHostData( metadataRequestContext: MetadataRequestContext, id: string, queryStrategyVersion?: MetadataQueryStrategyVersions -): Promise { - if ( - !metadataRequestContext.esClient && - !metadataRequestContext.requestHandlerContext?.core.elasticsearch.client - ) { - throw Boom.badRequest('esClient not found'); - } - - if ( - !metadataRequestContext.savedObjectsClient && - !metadataRequestContext.requestHandlerContext?.core.savedObjects - ) { - throw Boom.badRequest('savedObjectsClient not found'); - } - - const esClient = (metadataRequestContext?.esClient ?? - metadataRequestContext.requestHandlerContext?.core.elasticsearch - .client) as IScopedClusterClient; - - const esSavedObjectClient = - metadataRequestContext?.savedObjectsClient ?? - (metadataRequestContext.requestHandlerContext?.core.savedObjects - .client as SavedObjectsClientContract); - +): Promise { const queryStrategy = await metadataRequestContext.endpointAppContextService ?.getMetadataService() - ?.queryStrategy(esSavedObjectClient, queryStrategyVersion); - const query = getESQueryHostMetadataByID(id, queryStrategy!); - - const response = await esClient.asCurrentUser.search(query); - - const hostResult = queryStrategy!.queryResponseToHostResult(response.body); + ?.queryStrategy( + metadataRequestContext.requestHandlerContext.core.savedObjects.client, + queryStrategyVersion + ); + const query = getESQueryHostMetadataByID(id, queryStrategy!); + const hostResult = queryStrategy!.queryResponseToHostResult( + await metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client.callAsCurrentUser( + 'search', + query + ) + ); const hostMetadata = hostResult.result; if (!hostMetadata) { return undefined; } - return { metadata: hostMetadata, query_strategy_version: hostResult.queryStrategyVersion }; -} - -export async function getHostData( - metadataRequestContext: MetadataRequestContext, - id: string, - queryStrategyVersion?: MetadataQueryStrategyVersions -): Promise { - if (!metadataRequestContext.savedObjectsClient) { - throw Boom.badRequest('savedObjectsClient not found'); - } - - if ( - !metadataRequestContext.esClient && - !metadataRequestContext.requestHandlerContext?.core.elasticsearch.client - ) { - throw Boom.badRequest('esClient not found'); - } - - const hostResult = await getHostMetaData(metadataRequestContext, id, queryStrategyVersion); - - if (!hostResult) { - return undefined; - } - - const agent = await findAgent(metadataRequestContext, hostResult.metadata); + const agent = await findAgent(metadataRequestContext, hostMetadata); if (agent && !agent.active) { throw Boom.badRequest('the requested endpoint is unenrolled'); } const metadata = await enrichHostMetadata( - hostResult.metadata, + hostMetadata, metadataRequestContext, - hostResult.query_strategy_version + hostResult.queryStrategyVersion ); - - return { ...metadata, query_strategy_version: hostResult.query_strategy_version }; + return { ...metadata, query_strategy_version: hostResult.queryStrategyVersion }; } async function findAgent( @@ -265,20 +207,12 @@ async function findAgent( hostMetadata: HostMetadata ): Promise { try { - if ( - !metadataRequestContext.esClient && - !metadataRequestContext.requestHandlerContext?.core.elasticsearch.client - ) { - throw new Error('esClient not found'); - } - - const esClient = (metadataRequestContext?.esClient ?? - metadataRequestContext.requestHandlerContext?.core.elasticsearch - .client) as IScopedClusterClient; - return await metadataRequestContext.endpointAppContextService ?.getAgentService() - ?.getAgent(esClient.asCurrentUser, hostMetadata.elastic.agent.id); + ?.getAgent( + metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser, + hostMetadata.elastic.agent.id + ); } catch (e) { if (e instanceof AgentNotFoundError) { metadataRequestContext.logger.warn( @@ -298,7 +232,7 @@ export async function mapToHostResultList( metadataRequestContext: MetadataRequestContext ): Promise { const totalNumberOfHosts = hostListQueryResult.resultLength; - if ((hostListQueryResult.resultList?.length ?? 0) > 0) { + if (hostListQueryResult.resultList.length > 0) { return { request_page_size: queryParams.size, request_page_index: queryParams.from, @@ -333,35 +267,6 @@ export async function enrichHostMetadata( let hostStatus = HostStatus.UNHEALTHY; let elasticAgentId = hostMetadata?.elastic?.agent?.id; const log = metadataRequestContext.logger; - - try { - if ( - !metadataRequestContext.esClient && - !metadataRequestContext.requestHandlerContext?.core.elasticsearch.client - ) { - throw new Error('esClient not found'); - } - - if ( - !metadataRequestContext.savedObjectsClient && - !metadataRequestContext.requestHandlerContext?.core.savedObjects - ) { - throw new Error('esSavedObjectClient not found'); - } - } catch (e) { - log.error(e); - throw e; - } - - const esClient = (metadataRequestContext?.esClient ?? - metadataRequestContext.requestHandlerContext?.core.elasticsearch - .client) as IScopedClusterClient; - - const esSavedObjectClient = - metadataRequestContext?.savedObjectsClient ?? - (metadataRequestContext.requestHandlerContext?.core.savedObjects - .client as SavedObjectsClientContract); - try { /** * Get agent status by elastic agent id if available or use the endpoint-agent id. @@ -374,7 +279,10 @@ export async function enrichHostMetadata( const status = await metadataRequestContext.endpointAppContextService ?.getAgentService() - ?.getAgentStatusById(esClient.asCurrentUser, elasticAgentId); + ?.getAgentStatusById( + metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser, + elasticAgentId + ); hostStatus = HOST_STATUS_MAPPING.get(status!) || HostStatus.UNHEALTHY; } catch (e) { if (e instanceof AgentNotFoundError) { @@ -389,10 +297,17 @@ export async function enrichHostMetadata( try { const agent = await metadataRequestContext.endpointAppContextService ?.getAgentService() - ?.getAgent(esClient.asCurrentUser, elasticAgentId); + ?.getAgent( + metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser, + elasticAgentId + ); const agentPolicy = await metadataRequestContext.endpointAppContextService .getAgentPolicyService() - ?.get(esSavedObjectClient, agent?.policy_id!, true); + ?.get( + metadataRequestContext.requestHandlerContext.core.savedObjects.client, + agent?.policy_id!, + true + ); const endpointPolicy = ((agentPolicy?.package_policies || []) as PackagePolicy[]).find( (policy: PackagePolicy) => policy.package?.name === 'endpoint' ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index b916ec19da17f..f4698cbed6203 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -6,6 +6,8 @@ */ import { + ILegacyClusterClient, + ILegacyScopedClusterClient, KibanaResponseFactory, RequestHandler, RouteConfig, @@ -48,17 +50,12 @@ import { PackageService } from '../../../../../fleet/server/services'; import { metadataTransformPrefix } from '../../../../common/endpoint/constants'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; -import { - ClusterClientMock, - ScopedClusterClientMock, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../src/core/server/elasticsearch/client/mocks'; describe('test endpoint route', () => { let routerMock: jest.Mocked; let mockResponse: jest.Mocked; - let mockClusterClient: ClusterClientMock; - let mockScopedClient: ScopedClusterClientMock; + let mockClusterClient: jest.Mocked; + let mockScopedClient: jest.Mocked; let mockSavedObjectClient: jest.Mocked; let mockPackageService: jest.Mocked; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -79,8 +76,8 @@ describe('test endpoint route', () => { }; beforeEach(() => { - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); - mockClusterClient = elasticsearchServiceMock.createClusterClient(); + mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient() as jest.Mocked; mockSavedObjectClient = savedObjectsClientMock.create(); mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); @@ -122,9 +119,7 @@ describe('test endpoint route', () => { it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({}); const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) )!; @@ -136,7 +131,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -162,9 +157,7 @@ describe('test endpoint route', () => { mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: true, } as unknown) as Agent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) @@ -176,7 +169,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -221,9 +214,7 @@ describe('test endpoint route', () => { it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({}); const response = createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) )!; @@ -235,7 +226,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -267,10 +258,8 @@ describe('test endpoint route', () => { mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ - body: createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata()), - }) + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata())) ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) @@ -281,10 +270,10 @@ describe('test endpoint route', () => { mockRequest, mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect( - (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool - .must_not + mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must_not ).toContainEqual({ terms: { 'elastic.agent.id': [ @@ -326,10 +315,8 @@ describe('test endpoint route', () => { mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ - body: createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata()), - }) + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata())) ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) @@ -341,10 +328,10 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toBeCalled(); + expect(mockScopedClient.callAsCurrentUser).toBeCalled(); expect( // KQL filter to be passed through - (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must + mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must ).toContainEqual({ bool: { must_not: { @@ -362,7 +349,7 @@ describe('test endpoint route', () => { }, }); expect( - (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must + mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must ).toContainEqual({ bool: { must_not: [ @@ -406,8 +393,8 @@ describe('test endpoint route', () => { it('should return 404 on no results', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } }); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: createV2SearchResponse() }) + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createV2SearchResponse()) ); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); @@ -424,7 +411,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -444,9 +431,7 @@ describe('test endpoint route', () => { mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: true, } as unknown) as Agent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) @@ -458,7 +443,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -485,9 +470,7 @@ describe('test endpoint route', () => { SavedObjectsErrorHelpers.createGenericNotFoundError(); }); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) @@ -499,7 +482,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -520,9 +503,7 @@ describe('test endpoint route', () => { mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: true, } as unknown) as Agent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_ROUTE}`) @@ -534,7 +515,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -550,9 +531,7 @@ describe('test endpoint route', () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: response.hits.hits[0]._id }, }); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: false, } as unknown) as Agent); @@ -567,7 +546,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(mockResponse.customError).toBeCalled(); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts index 0d56514e7d395..e3f859c26601e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts @@ -6,17 +6,14 @@ */ import { + ILegacyClusterClient, + ILegacyScopedClusterClient, KibanaResponseFactory, RequestHandler, RouteConfig, SavedObjectsClientContract, - SavedObjectsErrorHelpers, -} from '../../../../../../../src/core/server'; -import { - ClusterClientMock, - ScopedClusterClientMock, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../src/core/server/elasticsearch/client/mocks'; +} from 'kibana/server'; +import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server/'; import { elasticsearchServiceMock, httpServerMock, @@ -52,8 +49,8 @@ import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; describe('test endpoint route v1', () => { let routerMock: jest.Mocked; let mockResponse: jest.Mocked; - let mockClusterClient: ClusterClientMock; - let mockScopedClient: ScopedClusterClientMock; + let mockClusterClient: jest.Mocked; + let mockScopedClient: jest.Mocked; let mockSavedObjectClient: jest.Mocked; let mockPackageService: jest.Mocked; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -74,8 +71,8 @@ describe('test endpoint route v1', () => { }; beforeEach(() => { - mockClusterClient = elasticsearchServiceMock.createClusterClient(); - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient() as jest.Mocked; + mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); @@ -113,9 +110,7 @@ describe('test endpoint route v1', () => { it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({}); const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) )!; @@ -127,7 +122,7 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] }); expect(mockResponse.ok).toBeCalled(); const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; @@ -156,10 +151,8 @@ describe('test endpoint route v1', () => { mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ - body: createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()), - }) + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata())) ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) @@ -171,10 +164,9 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect( - (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool - .must_not + mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must_not ).toContainEqual({ terms: { 'elastic.agent.id': [ @@ -213,10 +205,8 @@ describe('test endpoint route v1', () => { mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ - body: createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()), - }) + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata())) ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) @@ -228,10 +218,10 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toBeCalled(); + expect(mockScopedClient.callAsCurrentUser).toBeCalled(); // needs to have the KQL filter passed through expect( - (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must + mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must ).toContainEqual({ bool: { must_not: { @@ -250,7 +240,7 @@ describe('test endpoint route v1', () => { }); // and unenrolled should be filtered out. expect( - (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must + mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must ).toContainEqual({ bool: { must_not: [ @@ -291,8 +281,8 @@ describe('test endpoint route v1', () => { it('should return 404 on no results', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } }); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: createV1SearchResponse() }) + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createV1SearchResponse()) ); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); @@ -309,7 +299,7 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -329,9 +319,7 @@ describe('test endpoint route v1', () => { mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: true, } as unknown) as Agent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) @@ -343,7 +331,7 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -369,9 +357,7 @@ describe('test endpoint route v1', () => { SavedObjectsErrorHelpers.createGenericNotFoundError(); }); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) @@ -383,7 +369,7 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -404,9 +390,7 @@ describe('test endpoint route v1', () => { mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: true, } as unknown) as Agent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) @@ -418,7 +402,7 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'], @@ -434,9 +418,7 @@ describe('test endpoint route v1', () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: response.hits.hits[0]._id }, }); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); mockAgentService.getAgent = jest.fn().mockReturnValue(({ active: false, } as unknown) as Agent); @@ -451,7 +433,7 @@ describe('test endpoint route v1', () => { mockResponse ); - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(mockResponse.customError).toBeCalled(); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts index e790c1de1a5b8..5c09fd5ce05e4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts @@ -12,7 +12,6 @@ import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__ import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constants'; import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; import { metadataQueryStrategyV2 } from './support/query_strategies'; -import { get } from 'lodash'; describe('query builder', () => { describe('MetadataListESQuery', () => { @@ -205,7 +204,7 @@ describe('query builder', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV2()); - expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({ + expect(query.body.query.bool.filter[0].bool.should).toContainEqual({ term: { 'agent.id': mockID }, }); }); @@ -214,7 +213,7 @@ describe('query builder', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV2()); - expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({ + expect(query.body.query.bool.filter[0].bool.should).toContainEqual({ term: { 'HostDetails.agent.id': mockID }, }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index 51e3495938606..a5259dd44cf2b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { SearchRequest, SortContainer } from '@elastic/elasticsearch/api/types'; -import { KibanaRequest } from '../../../../../../../src/core/server'; +import { KibanaRequest } from 'kibana/server'; import { esKuery } from '../../../../../../../src/plugins/data/server'; import { EndpointAppContext, MetadataQueryStrategy } from '../../types'; @@ -20,7 +19,7 @@ export interface QueryBuilderOptions { // using unmapped_type avoids errors when the given field doesn't exist, and sets to the 0-value for that type // effectively ignoring it // https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html#_ignoring_unmapped_fields -const MetadataSortMethod: SortContainer[] = [ +const MetadataSortMethod = [ { 'event.created': { order: 'desc', @@ -147,7 +146,7 @@ function buildQueryBody( export function getESQueryHostMetadataByID( agentID: string, metadataQueryStrategy: MetadataQueryStrategy -): SearchRequest { +) { return { body: { query: { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts index c18c585cd3d34..9ce6130ff7dd3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts @@ -12,7 +12,6 @@ import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__ import { metadataIndexPattern } from '../../../../common/endpoint/constants'; import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; import { metadataQueryStrategyV1 } from './support/query_strategies'; -import { get } from 'lodash'; describe('query builder v1', () => { describe('MetadataListESQuery', () => { @@ -180,7 +179,7 @@ describe('query builder v1', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV1()); - expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({ + expect(query.body.query.bool.filter[0].bool.should).toContainEqual({ term: { 'agent.id': mockID }, }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts index 506c02fc2f1ec..2f875ec2754a4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SearchResponse } from '@elastic/elasticsearch/api/types'; +import { SearchResponse } from 'elasticsearch'; import { metadataCurrentIndexPattern, metadataIndexPattern, @@ -13,6 +13,10 @@ import { import { HostMetadata, MetadataQueryStrategyVersions } from '../../../../../common/endpoint/types'; import { HostListQueryResult, HostQueryResult, MetadataQueryStrategy } from '../../../types'; +interface HitSource { + _source: HostMetadata; +} + export function metadataQueryStrategyV1(): MetadataQueryStrategy { return { index: metadataIndexPattern, @@ -38,13 +42,11 @@ export function metadataQueryStrategyV1(): MetadataQueryStrategy { ): HostListQueryResult => { const response = searchResponse as SearchResponse; return { - resultLength: - ((response?.aggregations?.total as unknown) as { value?: number; relation: string }) - ?.value || 0, + resultLength: response?.aggregations?.total?.value || 0, resultList: response.hits.hits - .map((hit) => hit.inner_hits?.most_recent.hits.hits) - .flatMap((data) => data) - .map((entry) => (entry?._source ?? {}) as HostMetadata), + .map((hit) => hit.inner_hits.most_recent.hits.hits) + .flatMap((data) => data as HitSource) + .map((entry) => entry._source), queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_1, }; }, @@ -73,7 +75,7 @@ export function metadataQueryStrategyV2(): MetadataQueryStrategy { >; const list = response.hits.hits.length > 0 - ? response.hits.hits.map((entry) => stripHostDetails(entry?._source as HostMetadata)) + ? response.hits.hits.map((entry) => stripHostDetails(entry._source)) : []; return { @@ -93,7 +95,7 @@ export function metadataQueryStrategyV2(): MetadataQueryStrategy { resultLength: response.hits.hits.length, result: response.hits.hits.length > 0 - ? stripHostDetails(response.hits.hits[0]._source as HostMetadata) + ? stripHostDetails(response.hits.hits[0]._source) : undefined, queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_2, }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts index c8b36a22b359a..ca9b8832bebd0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts @@ -13,9 +13,10 @@ import { import { createMockAgentService } from '../../../../../fleet/server/mocks'; import { getHostPolicyResponseHandler, getAgentPolicySummaryHandler } from './handlers'; import { + ILegacyScopedClusterClient, KibanaResponseFactory, SavedObjectsClientContract, -} from '../../../../../../../src/core/server'; +} from 'kibana/server'; import { elasticsearchServiceMock, httpServerMock, @@ -29,19 +30,16 @@ import { parseExperimentalConfigValue } from '../../../../common/experimental_fe import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { Agent } from '../../../../../fleet/common/types/models'; import { AgentService } from '../../../../../fleet/server/services'; -import { get } from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ScopedClusterClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks'; describe('test policy response handler', () => { let endpointAppContextService: EndpointAppContextService; - let mockScopedClient: ScopedClusterClientMock; + let mockScopedClient: jest.Mocked; let mockSavedObjectClient: jest.Mocked; let mockResponse: jest.Mocked; describe('test policy response handler', () => { beforeEach(() => { - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockResponse = httpServerMock.createResponseFactory(); endpointAppContextService = new EndpointAppContextService(); @@ -54,9 +52,7 @@ describe('test policy response handler', () => { const response = createSearchResponse(new EndpointDocGenerator().generatePolicyResponse()); const hostPolicyResponseHandler = getHostPolicyResponseHandler(); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); const mockRequest = httpServerMock.createKibanaRequest({ params: { agentId: 'id' }, }); @@ -69,16 +65,14 @@ describe('test policy response handler', () => { expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as GetHostPolicyResponse; - expect(result.policy_response.agent.id).toEqual( - get(response, 'hits.hits.0._source.agent.id') - ); + expect(result.policy_response.agent.id).toEqual(response.hits.hits[0]._source.agent.id); }); it('should return not found when there is no response policy for host', async () => { const hostPolicyResponseHandler = getHostPolicyResponseHandler(); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: createSearchResponse() }) + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createSearchResponse()) ); const mockRequest = httpServerMock.createKibanaRequest({ @@ -115,7 +109,7 @@ describe('test policy response handler', () => { }; beforeEach(() => { - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockResponse = httpServerMock.createResponseFactory(); endpointAppContextService = new EndpointAppContextService(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts index 45b6201c47773..ec1fad80701b6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts @@ -25,7 +25,7 @@ export const getHostPolicyResponseHandler = function (): RequestHandler< const doc = await getPolicyResponseByAgentId( policyIndexPattern, request.query.agentId, - context.core.elasticsearch.client + context.core.elasticsearch.legacy.client ); if (doc) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts index 8646a05900f80..8043eae20b30e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts @@ -24,7 +24,7 @@ describe('test policy query', () => { it('queries for the correct host', async () => { const agentId = 'f757d3c0-e874-11ea-9ad9-015510b487f4'; const query = getESQueryPolicyResponseByAgentID(agentId, 'anyindex'); - expect(query.body?.query?.bool?.filter).toEqual({ term: { 'agent.id': agentId } }); + expect(query.body.query.bool.filter.term).toEqual({ 'agent.id': agentId }); }); it('filters out initial policy by ID', async () => { @@ -32,10 +32,8 @@ describe('test policy query', () => { 'f757d3c0-e874-11ea-9ad9-015510b487f4', 'anyindex' ); - expect(query.body?.query?.bool?.must_not).toEqual({ - term: { - 'Endpoint.policy.applied.id': '00000000-0000-0000-0000-000000000000', - }, + expect(query.body.query.bool.must_not.term).toEqual({ + 'Endpoint.policy.applied.id': '00000000-0000-0000-0000-000000000000', }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts index 987bef15afe98..af5a885b78040 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts @@ -5,21 +5,18 @@ * 2.0. */ +import { SearchResponse } from 'elasticsearch'; import { ElasticsearchClient, - IScopedClusterClient, + ILegacyScopedClusterClient, SavedObjectsClientContract, -} from '../../../../../../../src/core/server'; +} from 'kibana/server'; import { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types'; import { INITIAL_POLICY_ID } from './index'; import { Agent } from '../../../../../fleet/common/types/models'; import { EndpointAppContext } from '../../types'; -import { ISearchRequestParams } from '../../../../../../../src/plugins/data/common'; -export const getESQueryPolicyResponseByAgentID = ( - agentID: string, - index: string -): ISearchRequestParams => { +export function getESQueryPolicyResponseByAgentID(agentID: string, index: string) { return { body: { query: { @@ -47,23 +44,26 @@ export const getESQueryPolicyResponseByAgentID = ( }, index, }; -}; +} export async function getPolicyResponseByAgentId( index: string, agentID: string, - dataClient: IScopedClusterClient + dataClient: ILegacyScopedClusterClient ): Promise { const query = getESQueryPolicyResponseByAgentID(agentID, index); - const response = await dataClient.asCurrentUser.search(query); + const response = (await dataClient.callAsCurrentUser( + 'search', + query + )) as SearchResponse; - if (response.body.hits.hits.length > 0 && response.body.hits.hits[0]._source != null) { - return { - policy_response: response.body.hits.hits[0]._source, - }; + if (response.hits.hits.length === 0) { + return undefined; } - return undefined; + return { + policy_response: response.hits.hits[0]._source, + }; } const transformAgentVersionMap = (versionMap: Map): { [key: string]: number } => { diff --git a/x-pack/plugins/security_solution/server/endpoint/types.ts b/x-pack/plugins/security_solution/server/endpoint/types.ts index b3c7e58afe991..8006bf20d4517 100644 --- a/x-pack/plugins/security_solution/server/endpoint/types.ts +++ b/x-pack/plugins/security_solution/server/endpoint/types.ts @@ -6,8 +6,7 @@ */ import { LoggerFactory } from 'kibana/server'; - -import { SearchResponse } from '@elastic/elasticsearch/api/types'; +import { SearchResponse } from 'elasticsearch'; import { ConfigType } from '../config'; import { EndpointAppContextService } from './endpoint_app_context_services'; import { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 158c2e94b2d7a..46467a21ca7ab 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -305,10 +305,7 @@ export class Plugin implements IPlugin { - const securitySolutionSearchStrategy = securitySolutionSearchStrategyProvider( - depsStart.data, - endpointContext - ); + const securitySolutionSearchStrategy = securitySolutionSearchStrategyProvider(depsStart.data); const securitySolutionTimelineSearchStrategy = securitySolutionTimelineSearchStrategyProvider( depsStart.data ); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx index fa78a8d59803d..9e85eefe21e8a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx @@ -58,6 +58,7 @@ export const authentications: SecuritySolutionFactory fakeTotalCount; + return { ...response, inspect, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts index 9dfff5e11715d..7561682e070fc 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts @@ -1370,20 +1370,6 @@ export const formattedSearchStrategyResponse = { terms: { field: 'cloud.region', size: 10, order: { timestamp: 'desc' } }, aggs: { timestamp: { max: { field: '@timestamp' } } }, }, - endpoint_id: { - filter: { - term: { - 'agent.type': 'endpoint', - }, - }, - aggs: { - value: { - terms: { - field: 'agent.id', - }, - }, - }, - }, }, query: { bool: { @@ -1427,20 +1413,6 @@ export const expectedDsl = { track_total_hits: false, body: { aggregations: { - endpoint_id: { - filter: { - term: { - 'agent.type': 'endpoint', - }, - }, - aggs: { - value: { - terms: { - field: 'agent.id', - }, - }, - }, - }, host_architecture: { terms: { field: 'host.architecture', diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index 1b6e927f33638..a581370cb5720 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -7,23 +7,16 @@ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; -import { - IScopedClusterClient, - SavedObjectsClientContract, -} from '../../../../../../../../../src/core/server'; import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; +import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; import { Direction } from '../../../../../../common/search_strategy/common'; import { AggregationRequest, - EndpointFields, HostAggEsItem, HostBuckets, HostItem, HostValue, } from '../../../../../../common/search_strategy/security_solution/hosts'; -import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; -import { getHostMetaData } from '../../../../../endpoint/routes/metadata/handlers'; -import { EndpointAppContext } from '../../../../../endpoint/types'; export const HOST_FIELDS = [ '_id', @@ -45,8 +38,6 @@ export const HOST_FIELDS = [ 'endpoint.endpointPolicy', 'endpoint.policyStatus', 'endpoint.sensorVersion', - 'agent.type', - 'endpoint.id', ]; export const buildFieldsTermAggregation = (esFields: readonly string[]): AggregationRequest => @@ -108,8 +99,8 @@ const getTermsAggregationTypeFromField = (field: string): AggregationRequest => }; }; -export const formatHostItem = (bucket: HostAggEsItem): HostItem => { - return HOST_FIELDS.reduce((flattenedFields, fieldName) => { +export const formatHostItem = (bucket: HostAggEsItem): HostItem => + HOST_FIELDS.reduce((flattenedFields, fieldName) => { const fieldValue = getHostFieldValue(fieldName, bucket); if (fieldValue != null) { if (fieldName === '_id') { @@ -123,13 +114,11 @@ export const formatHostItem = (bucket: HostAggEsItem): HostItem => { } return flattenedFields; }, {}); -}; const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | string[] | null => { const aggField = hostFieldsMap[fieldName] ? hostFieldsMap[fieldName].replace(/\./g, '_') : fieldName.replace(/\./g, '_'); - if ( [ 'host.ip', @@ -145,7 +134,10 @@ const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | s return data.buckets.map((obj) => obj.key); } else if (has(`${aggField}.buckets`, bucket)) { return getFirstItem(get(`${aggField}`, bucket)); - } else if (['host.name', 'host.os.name', 'host.os.version', 'endpoint.id'].includes(fieldName)) { + } else if (has(aggField, bucket)) { + const valueObj: HostValue = get(aggField, bucket); + return valueObj.value_as_string; + } else if (['host.name', 'host.os.name', 'host.os.version'].includes(fieldName)) { switch (fieldName) { case 'host.name': return get('key', bucket) || null; @@ -153,12 +145,7 @@ const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | s return get('os.hits.hits[0]._source.host.os.name', bucket) || null; case 'host.os.version': return get('os.hits.hits[0]._source.host.os.version', bucket) || null; - case 'endpoint.id': - return get('endpoint_id.value.buckets[0].key', bucket) || null; } - } else if (has(aggField, bucket)) { - const valueObj: HostValue = get(aggField, bucket); - return valueObj.value_as_string; } else if (aggField === '_id') { const hostName = get(`host_name`, bucket); return hostName ? getFirstItem(hostName) : null; @@ -173,42 +160,3 @@ const getFirstItem = (data: HostBuckets): string | null => { } return firstItem.key; }; - -export const getHostEndpoint = async ( - id: string | null, - deps: { - esClient: IScopedClusterClient; - savedObjectsClient: SavedObjectsClientContract; - endpointContext: EndpointAppContext; - } -): Promise => { - const { esClient, endpointContext, savedObjectsClient } = deps; - const logger = endpointContext.logFactory.get('metadata'); - try { - const agentService = endpointContext.service.getAgentService(); - if (agentService === undefined) { - throw new Error('agentService not available'); - } - const metadataRequestContext = { - esClient, - endpointAppContextService: endpointContext.service, - logger, - savedObjectsClient, - }; - const endpointData = - id != null && metadataRequestContext.endpointAppContextService.getAgentService() != null - ? await getHostMetaData(metadataRequestContext, id, undefined) - : null; - - return endpointData != null && endpointData.metadata - ? { - endpointPolicy: endpointData.metadata.Endpoint.policy.applied.name, - policyStatus: endpointData.metadata.Endpoint.policy.applied.status, - sensorVersion: endpointData.metadata.agent.version, - } - : null; - } catch (err) { - logger.warn(JSON.stringify(err, null, 2)); - return null; - } -}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx index 4474b9f288570..244b826c7caeb 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx @@ -12,32 +12,6 @@ import { mockSearchStrategyResponse, formattedSearchStrategyResponse, } from './__mocks__'; -import { - IScopedClusterClient, - SavedObjectsClientContract, -} from '../../../../../../../../../src/core/server'; -import { EndpointAppContext } from '../../../../../endpoint/types'; -import { EndpointAppContextService } from '../../../../../endpoint/endpoint_app_context_services'; - -const mockDeps = { - esClient: {} as IScopedClusterClient, - savedObjectsClient: {} as SavedObjectsClientContract, - endpointContext: { - logFactory: { - get: jest.fn().mockReturnValue({ - warn: jest.fn(), - }), - }, - config: jest.fn().mockResolvedValue({}), - experimentalFeatures: { - trustedAppsByPolicyEnabled: false, - metricsEntitiesEnabled: false, - eventFilteringEnabled: false, - hostIsolationEnabled: false, - }, - service: {} as EndpointAppContextService, - } as EndpointAppContext, -}; describe('hostDetails search strategy', () => { const buildHostDetailsQuery = jest.spyOn(buildQuery, 'buildHostDetailsQuery'); @@ -55,7 +29,7 @@ describe('hostDetails search strategy', () => { describe('parse', () => { test('should parse data correctly', async () => { - const result = await hostDetails.parse(mockOptions, mockSearchStrategyResponse, mockDeps); + const result = await hostDetails.parse(mockOptions, mockSearchStrategyResponse); expect(result).toMatchObject(formattedSearchStrategyResponse); }); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts index 562b7e4fbc167..5da64cc8f7a90 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts @@ -10,58 +10,28 @@ import { get } from 'lodash/fp'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { HostAggEsData, + HostAggEsItem, HostDetailsStrategyResponse, HostsQueries, HostDetailsRequestOptions, - EndpointFields, } from '../../../../../../common/search_strategy/security_solution/hosts'; import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionFactory } from '../../types'; import { buildHostDetailsQuery } from './query.host_details.dsl'; -import { formatHostItem, getHostEndpoint } from './helpers'; -import { EndpointAppContext } from '../../../../../endpoint/types'; -import { - IScopedClusterClient, - SavedObjectsClientContract, -} from '../../../../../../../../../src/core/server'; +import { formatHostItem } from './helpers'; export const hostDetails: SecuritySolutionFactory = { buildDsl: (options: HostDetailsRequestOptions) => buildHostDetailsQuery(options), parse: async ( options: HostDetailsRequestOptions, - response: IEsSearchResponse, - deps?: { - esClient: IScopedClusterClient; - savedObjectsClient: SavedObjectsClientContract; - endpointContext: EndpointAppContext; - } + response: IEsSearchResponse ): Promise => { - const aggregations = get('aggregations', response.rawResponse); - + const aggregations: HostAggEsItem = get('aggregations', response.rawResponse) || {}; const inspect = { dsl: [inspectStringifyObject(buildHostDetailsQuery(options))], }; - - if (aggregations == null) { - return { ...response, inspect, hostDetails: {} }; - } - const formattedHostItem = formatHostItem(aggregations); - const ident = // endpoint-generated ID, NOT elastic-agent-id - formattedHostItem.endpoint && formattedHostItem.endpoint.id - ? Array.isArray(formattedHostItem.endpoint.id) - ? formattedHostItem.endpoint.id[0] - : formattedHostItem.endpoint.id - : null; - if (deps == null) { - return { ...response, inspect, hostDetails: { ...formattedHostItem } }; - } - const endpoint: EndpointFields | null = await getHostEndpoint(ident, deps); - return { - ...response, - inspect, - hostDetails: endpoint != null ? { ...formattedHostItem, endpoint } : formattedHostItem, - }; + return { ...response, inspect, hostDetails: formattedHostItem }; }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts index 45afed2526aa3..fb8296d6593b0 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts @@ -16,10 +16,7 @@ export const buildHostDetailsQuery = ({ defaultIndex, timerange: { from, to }, }: HostDetailsRequestOptions): ISearchRequestParams => { - const esFields = reduceFields(HOST_FIELDS, { - ...hostFieldsMap, - ...cloudFieldsMap, - }); + const esFields = reduceFields(HOST_FIELDS, { ...hostFieldsMap, ...cloudFieldsMap }); const filter = [ { term: { 'host.name': hostName } }, @@ -42,20 +39,6 @@ export const buildHostDetailsQuery = ({ body: { aggregations: { ...buildFieldsTermAggregation(esFields.filter((field) => !['@timestamp'].includes(field))), - endpoint_id: { - filter: { - term: { - 'agent.type': 'endpoint', - }, - }, - aggs: { - value: { - terms: { - field: 'agent.id', - }, - }, - }, - }, }, query: { bool: { filter } }, size: 0, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts index 4bdf97b489805..3455b627144bf 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts @@ -5,10 +5,6 @@ * 2.0. */ -import { - IScopedClusterClient, - SavedObjectsClientContract, -} from '../../../../../../../src/core/server'; import { IEsSearchResponse, ISearchRequestParams, @@ -18,17 +14,11 @@ import { StrategyRequestType, StrategyResponseType, } from '../../../../common/search_strategy/security_solution'; -import { EndpointAppContext } from '../../../endpoint/types'; export interface SecuritySolutionFactory { buildDsl: (options: StrategyRequestType) => ISearchRequestParams; parse: ( options: StrategyRequestType, - response: IEsSearchResponse, - deps?: { - esClient: IScopedClusterClient; - savedObjectsClient: SavedObjectsClientContract; - endpointContext: EndpointAppContext; - } + response: IEsSearchResponse ) => Promise>; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts index 0883a144615bc..2980f63df8a67 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts @@ -19,11 +19,9 @@ import { } from '../../../common/search_strategy/security_solution'; import { securitySolutionFactory } from './factory'; import { SecuritySolutionFactory } from './factory/types'; -import { EndpointAppContext } from '../../endpoint/types'; export const securitySolutionSearchStrategyProvider = ( - data: PluginStart, - endpointContext: EndpointAppContext + data: PluginStart ): ISearchStrategy, StrategyResponseType> => { const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); @@ -44,13 +42,7 @@ export const securitySolutionSearchStrategyProvider = - queryFactory.parse(request, esSearchRes, { - esClient: deps.esClient, - savedObjectsClient: deps.savedObjectsClient, - endpointContext, - }) - ) + mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes)) ); }, cancel: async (id, options, deps) => { From fcc2ac5799d48444bc8c30ef25c26f49b0b7e76d Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 10 May 2021 15:24:42 -0700 Subject: [PATCH 28/69] Fixed alerting health check behavior when alerting cannot find its health task in Task Manager. (#99564) * Fixed alerting health check behavior when alerting cannot find its health task in Task Manager. * fixed test * added unit tests --- .../alerting/server/health/get_health.test.ts | 74 +++++++- .../alerting/server/health/get_health.ts | 15 +- .../alerting/server/health/get_state.test.ts | 166 ++++++++++++++++-- .../alerting/server/health/get_state.ts | 61 +++++-- x-pack/plugins/alerting/server/health/task.ts | 13 +- x-pack/plugins/alerting/server/plugin.ts | 9 +- 6 files changed, 290 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/alerting/server/health/get_health.test.ts b/x-pack/plugins/alerting/server/health/get_health.test.ts index 3c494dac6785b..c31a71138248b 100644 --- a/x-pack/plugins/alerting/server/health/get_health.test.ts +++ b/x-pack/plugins/alerting/server/health/get_health.test.ts @@ -5,9 +5,12 @@ * 2.0. */ -import { savedObjectsRepositoryMock } from '../../../../../src/core/server/mocks'; +import { + savedObjectsRepositoryMock, + savedObjectsServiceMock, +} from '../../../../../src/core/server/mocks'; import { AlertExecutionStatusErrorReasons, HealthStatus } from '../types'; -import { getHealth } from './get_health'; +import { getAlertingHealthStatus, getHealth } from './get_health'; const savedObjectsRepository = savedObjectsRepositoryMock.create(); @@ -221,3 +224,70 @@ describe('getHealth()', () => { }); }); }); + +describe('getAlertingHealthStatus()', () => { + test('return the proper framework state if some of alerts has a decryption error', async () => { + const savedObjects = savedObjectsServiceMock.createStartContract(); + const lastExecutionDateError = new Date().toISOString(); + savedObjectsRepository.find.mockResolvedValueOnce({ + total: 1, + per_page: 1, + page: 1, + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + executionStatus: { + status: 'error', + lastExecutionDate: lastExecutionDateError, + error: { + reason: AlertExecutionStatusErrorReasons.Decrypt, + message: 'Failed decrypt', + }, + }, + }, + score: 1, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }, + ], + }); + savedObjectsRepository.find.mockResolvedValue({ + total: 0, + per_page: 10, + page: 1, + saved_objects: [], + }); + const result = await getAlertingHealthStatus( + { ...savedObjects, createInternalRepository: () => savedObjectsRepository }, + 1 + ); + expect(result).toStrictEqual({ + state: { + runs: 2, + health_status: HealthStatus.Warning, + }, + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/health/get_health.ts b/x-pack/plugins/alerting/server/health/get_health.ts index f00e79a0d96ea..4a0266c9b729f 100644 --- a/x-pack/plugins/alerting/server/health/get_health.ts +++ b/x-pack/plugins/alerting/server/health/get_health.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ISavedObjectsRepository } from 'src/core/server'; +import { ISavedObjectsRepository, SavedObjectsServiceStart } from 'src/core/server'; import { AlertsHealth, HealthStatus, RawAlert, AlertExecutionStatusErrorReasons } from '../types'; export const getHealth = async ( @@ -97,3 +97,16 @@ export const getHealth = async ( return healthStatuses; }; + +export const getAlertingHealthStatus = async ( + savedObjects: SavedObjectsServiceStart, + stateRuns?: number +) => { + const alertingHealthStatus = await getHealth(savedObjects.createInternalRepository(['alert'])); + return { + state: { + runs: (stateRuns || 0) + 1, + health_status: alertingHealthStatus.decryptionHealth.status, + }, + }; +}; diff --git a/x-pack/plugins/alerting/server/health/get_state.test.ts b/x-pack/plugins/alerting/server/health/get_state.test.ts index 7b36bf34377f7..643d966d1fad0 100644 --- a/x-pack/plugins/alerting/server/health/get_state.test.ts +++ b/x-pack/plugins/alerting/server/health/get_state.test.ts @@ -14,6 +14,16 @@ import { } from './get_state'; import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager/server'; import { HealthStatus } from '../types'; +import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; + +jest.mock('./get_health', () => ({ + getAlertingHealthStatus: jest.fn().mockReturnValue({ + state: { + runs: 0, + health_status: 'warn', + }, + }), +})); const tick = () => new Promise((resolve) => setImmediate(resolve)); @@ -38,6 +48,9 @@ const getHealthCheckTask = (overrides = {}): ConcreteTaskInstance => ({ ...overrides, }); +const logger = loggingSystemMock.create().get(); +const savedObjects = savedObjectsServiceMock.createStartContract(); + describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { beforeEach(() => jest.useFakeTimers()); @@ -47,7 +60,21 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { const pollInterval = 100; const halfInterval = Math.floor(pollInterval / 2); - getHealthStatusStream(mockTaskManager, pollInterval).subscribe(); + getHealthStatusStream( + mockTaskManager, + logger, + savedObjects, + Promise.resolve({ + healthCheck: { + interval: '5m', + }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '1h', + }, + }), + pollInterval + ).subscribe(); // shouldn't fire before poll interval passes // should fire once each poll interval @@ -68,7 +95,22 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { const pollInterval = 100; const halfInterval = Math.floor(pollInterval / 2); - getHealthStatusStream(mockTaskManager, pollInterval, retryDelay).subscribe(); + getHealthStatusStream( + mockTaskManager, + logger, + savedObjects, + Promise.resolve({ + healthCheck: { + interval: '5m', + }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '1h', + }, + }), + pollInterval, + retryDelay + ).subscribe(); jest.advanceTimersByTime(halfInterval); expect(mockTaskManager.get).toHaveBeenCalledTimes(0); @@ -99,7 +141,18 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { mockTaskManager.get.mockResolvedValue(getHealthCheckTask()); const status = await getHealthServiceStatusWithRetryAndErrorHandling( - mockTaskManager + mockTaskManager, + logger, + savedObjects, + Promise.resolve({ + healthCheck: { + interval: '5m', + }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '1h', + }, + }) ).toPromise(); expect(status.level).toEqual(ServiceStatusLevels.available); @@ -118,7 +171,18 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { ); const status = await getHealthServiceStatusWithRetryAndErrorHandling( - mockTaskManager + mockTaskManager, + logger, + savedObjects, + Promise.resolve({ + healthCheck: { + interval: '5m', + }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '1h', + }, + }) ).toPromise(); expect(status.level).toEqual(ServiceStatusLevels.degraded); @@ -137,7 +201,18 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { ); const status = await getHealthServiceStatusWithRetryAndErrorHandling( - mockTaskManager + mockTaskManager, + logger, + savedObjects, + Promise.resolve({ + healthCheck: { + interval: '5m', + }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '1h', + }, + }) ).toPromise(); expect(status.level).toEqual(ServiceStatusLevels.unavailable); @@ -152,12 +227,25 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { .mockRejectedValueOnce(new Error('Failure')) .mockResolvedValue(getHealthCheckTask()); - getHealthServiceStatusWithRetryAndErrorHandling(mockTaskManager, retryDelay).subscribe( - (status) => { - expect(status.level).toEqual(ServiceStatusLevels.available); - expect(status.summary).toEqual('Alerting framework is available'); - } - ); + getHealthServiceStatusWithRetryAndErrorHandling( + mockTaskManager, + logger, + savedObjects, + Promise.resolve({ + healthCheck: { + interval: '5m', + }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '1h', + }, + }), + retryDelay + ).subscribe((status) => { + expect(status.level).toEqual(ServiceStatusLevels.available); + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(status.summary).toEqual('Alerting framework is available'); + }); await tick(); jest.advanceTimersByTime(retryDelay * 2); @@ -169,13 +257,25 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { const mockTaskManager = taskManagerMock.createStart(); mockTaskManager.get.mockRejectedValue(err); - getHealthServiceStatusWithRetryAndErrorHandling(mockTaskManager, retryDelay).subscribe( - (status) => { - expect(status.level).toEqual(ServiceStatusLevels.unavailable); - expect(status.summary).toEqual('Alerting framework is unavailable'); - expect(status.meta).toEqual({ error: err }); - } - ); + getHealthServiceStatusWithRetryAndErrorHandling( + mockTaskManager, + logger, + savedObjects, + Promise.resolve({ + healthCheck: { + interval: '5m', + }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '1h', + }, + }), + retryDelay + ).subscribe((status) => { + expect(status.level).toEqual(ServiceStatusLevels.unavailable); + expect(status.summary).toEqual('Alerting framework is unavailable'); + expect(status.meta).toEqual({ error: err }); + }); for (let i = 0; i < MAX_RETRY_ATTEMPTS + 1; i++) { await tick(); @@ -183,4 +283,34 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { } expect(mockTaskManager.get).toHaveBeenCalledTimes(MAX_RETRY_ATTEMPTS + 1); }); + + it('should schedule a new health check task if it does not exist without throwing an error', async () => { + const mockTaskManager = taskManagerMock.createStart(); + mockTaskManager.get.mockRejectedValue({ + output: { + statusCode: 404, + message: 'Not Found', + }, + }); + + const status = await getHealthServiceStatusWithRetryAndErrorHandling( + mockTaskManager, + logger, + savedObjects, + Promise.resolve({ + healthCheck: { + interval: '5m', + }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '1h', + }, + }) + ).toPromise(); + + expect(mockTaskManager.ensureScheduled).toHaveBeenCalledTimes(1); + expect(status.level).toEqual(ServiceStatusLevels.degraded); + expect(status.summary).toEqual('Alerting framework is degraded'); + expect(status.meta).toBeUndefined(); + }); }); diff --git a/x-pack/plugins/alerting/server/health/get_state.ts b/x-pack/plugins/alerting/server/health/get_state.ts index 5bd80f2c6d29f..30099614ea42b 100644 --- a/x-pack/plugins/alerting/server/health/get_state.ts +++ b/x-pack/plugins/alerting/server/health/get_state.ts @@ -8,27 +8,38 @@ import { i18n } from '@kbn/i18n'; import { defer, of, interval, Observable, throwError, timer } from 'rxjs'; import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators'; -import { ServiceStatus, ServiceStatusLevels } from '../../../../../src/core/server'; +import { + Logger, + SavedObjectsServiceStart, + ServiceStatus, + ServiceStatusLevels, +} from '../../../../../src/core/server'; import { TaskManagerStartContract } from '../../../task_manager/server'; -import { HEALTH_TASK_ID } from './task'; +import { HEALTH_TASK_ID, scheduleAlertingHealthCheck } from './task'; import { HealthStatus } from '../types'; +import { getAlertingHealthStatus } from './get_health'; +import { AlertsConfig } from '../config'; export const MAX_RETRY_ATTEMPTS = 3; const HEALTH_STATUS_INTERVAL = 60000 * 5; // Five minutes const RETRY_DELAY = 5000; // Wait 5 seconds before retrying on errors -async function getLatestTaskState(taskManager: TaskManagerStartContract) { +async function getLatestTaskState( + taskManager: TaskManagerStartContract, + logger: Logger, + savedObjects: SavedObjectsServiceStart, + config: Promise +) { try { - const result = await taskManager.get(HEALTH_TASK_ID); - return result; + return await taskManager.get(HEALTH_TASK_ID); } catch (err) { - const errMessage = err && err.message ? err.message : err.toString(); - if (!errMessage.includes('NotInitialized')) { - throw err; + // if task is not found + if (err?.output?.statusCode === 404) { + await scheduleAlertingHealthCheck(logger, config, taskManager); + return await getAlertingHealthStatus(savedObjects); } + throw err; } - - return null; } const LEVEL_SUMMARY = { @@ -53,13 +64,16 @@ const LEVEL_SUMMARY = { }; const getHealthServiceStatus = async ( - taskManager: TaskManagerStartContract + taskManager: TaskManagerStartContract, + logger: Logger, + savedObjects: SavedObjectsServiceStart, + config: Promise ): Promise> => { - const doc = await getLatestTaskState(taskManager); + const doc = await getLatestTaskState(taskManager, logger, savedObjects, config); const level = - doc?.state?.health_status === HealthStatus.OK + doc.state?.health_status === HealthStatus.OK ? ServiceStatusLevels.available - : doc?.state?.health_status === HealthStatus.Warning + : doc.state?.health_status === HealthStatus.Warning ? ServiceStatusLevels.degraded : ServiceStatusLevels.unavailable; return { @@ -70,9 +84,12 @@ const getHealthServiceStatus = async ( export const getHealthServiceStatusWithRetryAndErrorHandling = ( taskManager: TaskManagerStartContract, + logger: Logger, + savedObjects: SavedObjectsServiceStart, + config: Promise, retryDelay?: number ): Observable> => { - return defer(() => getHealthServiceStatus(taskManager)).pipe( + return defer(() => getHealthServiceStatus(taskManager, logger, savedObjects, config)).pipe( retryWhen((errors) => { return errors.pipe( mergeMap((error, i) => { @@ -85,6 +102,7 @@ export const getHealthServiceStatusWithRetryAndErrorHandling = ( ); }), catchError((error) => { + logger.warn(`Alerting framework is unavailable due to the error: ${error}`); return of({ level: ServiceStatusLevels.unavailable, summary: LEVEL_SUMMARY[ServiceStatusLevels.unavailable.toString()], @@ -96,9 +114,20 @@ export const getHealthServiceStatusWithRetryAndErrorHandling = ( export const getHealthStatusStream = ( taskManager: TaskManagerStartContract, + logger: Logger, + savedObjects: SavedObjectsServiceStart, + config: Promise, healthStatusInterval?: number, retryDelay?: number ): Observable> => interval(healthStatusInterval ?? HEALTH_STATUS_INTERVAL).pipe( - switchMap(() => getHealthServiceStatusWithRetryAndErrorHandling(taskManager, retryDelay)) + switchMap(() => + getHealthServiceStatusWithRetryAndErrorHandling( + taskManager, + logger, + savedObjects, + config, + retryDelay + ) + ) ); diff --git a/x-pack/plugins/alerting/server/health/task.ts b/x-pack/plugins/alerting/server/health/task.ts index a6f1237c43583..999e76fde696e 100644 --- a/x-pack/plugins/alerting/server/health/task.ts +++ b/x-pack/plugins/alerting/server/health/task.ts @@ -14,7 +14,7 @@ import { import { AlertsConfig } from '../config'; import { AlertingPluginsStart } from '../plugin'; import { HealthStatus } from '../types'; -import { getHealth } from './get_health'; +import { getAlertingHealthStatus } from './get_health'; export const HEALTH_TASK_TYPE = 'alerting_health_check'; @@ -71,15 +71,10 @@ export function healthCheckTaskRunner( return { async run() { try { - const alertingHealthStatus = await getHealth( - (await coreStartServices)[0].savedObjects.createInternalRepository(['alert']) + return await getAlertingHealthStatus( + (await coreStartServices)[0].savedObjects, + state.runs ); - return { - state: { - runs: (state.runs || 0) + 1, - health_status: alertingHealthStatus.decryptionHealth.status, - }, - }; } catch (errMsg) { logger.warn(`Error executing alerting health check task: ${errMsg}`); return { diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 1155cfa93337d..3d3b478c6480c 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -220,11 +220,16 @@ export class AlertingPlugin { this.config ); - core.getStartServices().then(async ([, startPlugins]) => { + core.getStartServices().then(async ([coreStart, startPlugins]) => { core.status.set( combineLatest([ core.status.derivedStatus$, - getHealthStatusStream(startPlugins.taskManager), + getHealthStatusStream( + startPlugins.taskManager, + this.logger, + coreStart.savedObjects, + this.config + ), ]).pipe( map(([derivedStatus, healthStatus]) => { if (healthStatus.level > derivedStatus.level) { From 0b5d323613c35b9159224b5b650c07ad99de1fed Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Mon, 10 May 2021 17:30:57 -0500 Subject: [PATCH 29/69] [Workplace Search] Fix bug with updating a role mapping (#99688) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `id` is not needed by the server as a body prop, as it’s inferred from the params. --- .../server/routes/workplace_search/role_mappings.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts index 8c7792f56fd6c..5a6359c1cd836 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts @@ -66,10 +66,7 @@ export function registerOrgRoleMappingRoute({ { path: '/api/workplace_search/org/role_mappings/{id}', validate: { - body: schema.object({ - ...roleMappingBaseSchema, - id: schema.string(), - }), + body: schema.object(roleMappingBaseSchema), params: schema.object({ id: schema.string(), }), From b5380697ccb9c2463d6ad96d1fa8e01a313093e5 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Mon, 10 May 2021 18:16:03 -0500 Subject: [PATCH 30/69] [Fleet] Add keep_enabled flag to preconfiguration to prevent disabling certain inputs (#99656) * [Fleet] Add keep_enabled flag to prevent disabling inputs * Remove console.log * Fix missing key definition --- x-pack/plugins/fleet/common/types/models/package_policy.ts | 2 ++ .../components/package_policy_input_panel.tsx | 1 + .../components/package_policy_input_stream.tsx | 1 + x-pack/plugins/fleet/server/services/package_policy.test.ts | 4 +++- x-pack/plugins/fleet/server/services/package_policy.ts | 2 ++ x-pack/plugins/fleet/server/services/preconfiguration.ts | 2 ++ x-pack/plugins/fleet/server/types/models/package_policy.ts | 2 ++ x-pack/plugins/fleet/server/types/models/preconfiguration.ts | 2 ++ 8 files changed, 15 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index f30cc0f87d05b..04362e6ff9402 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -21,6 +21,7 @@ export type PackagePolicyConfigRecord = Record } checked={packagePolicyInput.enabled} + disabled={packagePolicyInput.keep_enabled} onChange={(e) => { const enabled = e.target.checked; updatePackagePolicyInput({ diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx index 84f097813d484..5cc1fc4130256 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx @@ -83,6 +83,7 @@ export const PackagePolicyInputStreamConfig: React.FunctionComponent<{ { const enabled = e.target.checked; diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index 2516073793a8b..21241e6d5723f 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -390,6 +390,7 @@ describe('Package policy service', () => { { config: {}, enabled: true, + keep_enabled: true, type: 'endpoint', vars: { dog: { @@ -428,7 +429,7 @@ describe('Package policy service', () => { const inputsUpdate = [ { config: {}, - enabled: true, + enabled: false, type: 'endpoint', vars: { dog: { @@ -501,6 +502,7 @@ describe('Package policy service', () => { ); const [modifiedInput] = result.inputs; + expect(modifiedInput.enabled).toEqual(true); expect(modifiedInput.vars!.dog.value).toEqual('labrador'); expect(modifiedInput.vars!.cat.value).toEqual('siamese'); const [modifiedStream] = modifiedInput.streams; diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 234fa4df51688..e4d34e551c95b 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -614,12 +614,14 @@ function enforceFrozenInputs(oldInputs: PackagePolicyInput[], newInputs: Package for (const input of resultInputs) { const oldInput = oldInputs.find((i) => i.type === input.type); + if (oldInput?.keep_enabled) input.enabled = oldInput.enabled; if (input.vars && oldInput?.vars) { input.vars = _enforceFrozenVars(oldInput.vars, input.vars); } if (input.streams && oldInput?.streams) { for (const stream of input.streams) { const oldStream = oldInput.streams.find((s) => s.id === stream.id); + if (oldStream?.keep_enabled) stream.enabled = oldStream.enabled; if (stream.vars && oldStream?.vars) { stream.vars = _enforceFrozenVars(oldStream.vars, stream.vars); } diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 308abece9f4f5..a8be94ca61c0a 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -285,6 +285,8 @@ function overridePackageInputs( } if (typeof override.enabled !== 'undefined') originalInput.enabled = override.enabled; + if (typeof override.keep_enabled !== 'undefined') + originalInput.keep_enabled = override.keep_enabled; if (override.vars) { try { diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index 5a8fd70a9b84e..fa467a4185bd4 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -46,6 +46,7 @@ const PackagePolicyBaseSchema = { schema.object({ type: schema.string(), enabled: schema.boolean(), + keep_enabled: schema.maybe(schema.boolean()), vars: schema.maybe(ConfigRecordSchema), config: schema.maybe( schema.recordOf( @@ -60,6 +61,7 @@ const PackagePolicyBaseSchema = { schema.object({ id: schema.maybe(schema.string()), // BWC < 7.11 enabled: schema.boolean(), + keep_enabled: schema.maybe(schema.boolean()), data_stream: schema.object({ dataset: schema.string(), type: schema.string() }), vars: schema.maybe(ConfigRecordSchema), config: schema.maybe( diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index 5b871b80a6bbd..e988283c4aad9 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -67,6 +67,7 @@ export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( schema.object({ type: schema.string(), enabled: schema.maybe(schema.boolean()), + keep_enabled: schema.maybe(schema.boolean()), vars: varsSchema, streams: schema.maybe( schema.arrayOf( @@ -76,6 +77,7 @@ export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( dataset: schema.string(), }), enabled: schema.maybe(schema.boolean()), + keep_enabled: schema.maybe(schema.boolean()), vars: varsSchema, }) ) From b248472e82cd736a68170eca3d1d9e8115c2ee71 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 10 May 2021 17:46:33 -0600 Subject: [PATCH 31/69] [Maps] show empty tooltips with actions on click (#99337) * [Maps] show tooltips with actions on click * clean up * call canShowTooltip in _getTooltipFeatures * move view into action * cleanup * i18n * add comment to clarify if statement * tslint * fix security tslint * fix jest tests * clean up Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__stories__/MapTooltip.stories.tsx | 1 + .../common/descriptor_types/map_descriptor.ts | 12 ++ .../maps/public/actions/tooltip_actions.ts | 15 +- .../maps/public/classes/layers/layer.tsx | 5 - .../layers/vector_layer/vector_layer.tsx | 6 +- .../ems_file_source/ems_file_source.tsx | 2 +- .../es_geo_grid_source/es_geo_grid_source.tsx | 2 +- .../es_geo_line_source/es_geo_line_source.tsx | 2 +- .../es_pew_pew_source/es_pew_pew_source.js | 2 +- .../es_search_source/es_search_source.tsx | 2 +- .../geojson_file_source.ts | 2 +- .../kibana_regionmap_source.ts | 2 +- .../mvt_single_layer_vector_source.test.tsx | 6 +- .../mvt_single_layer_vector_source.tsx | 2 +- .../sources/table_source/table_source.ts | 2 +- .../sources/vector_source/vector_source.tsx | 4 +- .../features_tooltip/features_tooltip.tsx | 131 ++++----------- .../mb_map/features_tooltip/footer.test.tsx | 6 + .../mb_map/features_tooltip/index.ts | 9 ++ .../tooltip_control.test.js.snap | 30 +--- .../tooltip_popover.test.js.snap | 9 -- .../mb_map/tooltip_control/tooltip_control.js | 153 ++++++++++++++++-- .../tooltip_control/tooltip_control.test.js | 26 ++- .../mb_map/tooltip_control/tooltip_popover.js | 50 +----- .../map_tool_tip/map_tool_tip.test.tsx | 1 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 27 files changed, 247 insertions(+), 237 deletions(-) create mode 100644 x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/index.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx index b96fb42c3d1f4..1aad25fc89c0b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx @@ -44,6 +44,7 @@ storiesOf('app/RumDashboard/VisitorsRegionMap', module) '__kbnjoin__count__3657625d-17b0-41ef-99ba-3a2b2938655c': 439145, '__kbnjoin__avg_of_transaction.duration.us__3657625d-17b0-41ef-99ba-3a2b2938655c': 2041665.6350131081, }, + actions: [], }, ]} /> diff --git a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts index acb0d9329e065..0bb9c7cc6f02b 100644 --- a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts +++ b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts @@ -7,7 +7,9 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ +import { ReactNode } from 'react'; import { GeoJsonProperties } from 'geojson'; +import { Geometry } from 'geojson'; import { Query } from '../../../../../src/plugins/data/common'; import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants'; @@ -41,10 +43,20 @@ export type Goto = { center?: MapCenterAndZoom; }; +export const GEOMETRY_FILTER_ACTION = 'GEOMETRY_FILTER_ACTION'; + +export type TooltipFeatureAction = { + label: string; + id: typeof GEOMETRY_FILTER_ACTION; + form: ReactNode; +}; + export type TooltipFeature = { id?: number | string; layerId: string; + geometry?: Geometry; mbProperties: GeoJsonProperties; + actions: TooltipFeatureAction[]; }; export type TooltipState = { diff --git a/x-pack/plugins/maps/public/actions/tooltip_actions.ts b/x-pack/plugins/maps/public/actions/tooltip_actions.ts index 365a906739991..c1b5f8190a73a 100644 --- a/x-pack/plugins/maps/public/actions/tooltip_actions.ts +++ b/x-pack/plugins/maps/public/actions/tooltip_actions.ts @@ -6,7 +6,6 @@ */ import _ from 'lodash'; -import uuid from 'uuid/v4'; import { Dispatch } from 'redux'; import { Feature } from 'geojson'; import { getOpenTooltips } from '../selectors/map_selectors'; @@ -36,11 +35,7 @@ export function openOnClickTooltip(tooltipState: TooltipState) { ); }); - openTooltips.push({ - ...tooltipState, - isLocked: true, - id: uuid(), - }); + openTooltips.push(tooltipState); dispatch({ type: SET_OPEN_TOOLTIPS, @@ -63,13 +58,7 @@ export function closeOnHoverTooltip() { export function openOnHoverTooltip(tooltipState: TooltipState) { return { type: SET_OPEN_TOOLTIPS, - openTooltips: [ - { - ...tooltipState, - isLocked: false, - id: uuid(), - }, - ], + openTooltips: [tooltipState], }; } diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index dee1a26efef42..4167ed4775219 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -77,7 +77,6 @@ export interface ILayer { getMbLayerIds(): string[]; ownsMbLayerId(mbLayerId: string): boolean; ownsMbSourceId(mbSourceId: string): boolean; - canShowTooltip(): boolean; syncLayerWithMB(mbMap: MbMap): void; getLayerTypeIconName(): string; isInitialDataLoadComplete(): boolean; @@ -452,10 +451,6 @@ export class AbstractLayer implements ILayer { throw new Error('Should implement AbstractLayer#ownsMbSourceId'); } - canShowTooltip() { - return false; - } - syncLayerWithMB(mbMap: MbMap) { throw new Error('Should implement AbstractLayer#syncLayerWithMB'); } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 104d0b56578d1..db781344eb6f8 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -88,6 +88,7 @@ export interface IVectorLayer extends ILayer { getFeatureById(id: string | number): Feature | null; getPropertiesForTooltip(properties: GeoJsonProperties): Promise; hasJoins(): boolean; + canShowTooltip(): boolean; } export class VectorLayer extends AbstractLayer implements IVectorLayer { @@ -1033,10 +1034,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } canShowTooltip() { - return ( - this.isVisible() && - (this.getSource().canFormatFeatureProperties() || this.getJoins().length > 0) - ); + return this.getSource().hasTooltipProperties() || this.getJoins().length > 0; } getFeatureById(id: string | number) { diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx index a61ae85c89ac6..209dc43f504d1 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx @@ -196,7 +196,7 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc return fields.map((f) => this.createField({ fieldName: f.name })); } - canFormatFeatureProperties() { + hasTooltipProperties() { return this._tooltipFields.length > 0; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx index 7bca22df9b870..69ec0740948fc 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx @@ -471,7 +471,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle } } - canFormatFeatureProperties(): boolean { + hasTooltipProperties(): boolean { return true; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx index 9a1f23e055af1..460c1228e50a8 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx @@ -359,7 +359,7 @@ export class ESGeoLineSource extends AbstractESAggSource { return true; } - canFormatFeatureProperties() { + hasTooltipProperties() { return true; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index 781cc7f8c36b0..7ed24b4805997 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -226,7 +226,7 @@ export class ESPewPewSource extends AbstractESAggSource { return turfBboxToBounds(turfBbox(multiPoint(corners))); } - canFormatFeatureProperties() { + hasTooltipProperties() { return true; } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 3de98fd545827..8a6e97bf2a1af 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -464,7 +464,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye }; } - canFormatFeatureProperties(): boolean { + hasTooltipProperties(): boolean { return this._tooltipFields.length > 0; } diff --git a/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts b/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts index 363e19fdb1587..592c2f852f0e7 100644 --- a/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts @@ -121,7 +121,7 @@ export class GeoJsonFileSource extends AbstractVectorSource { return (this._descriptor as GeojsonFileSourceDescriptor).name; } - canFormatFeatureProperties() { + hasTooltipProperties() { return true; } diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts index 12e4b00c3c7b9..b0241876e5728 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts @@ -104,7 +104,7 @@ export class KibanaRegionmapSource extends AbstractVectorSource { return this._descriptor.name; } - canFormatFeatureProperties() { + hasTooltipProperties() { return true; } } diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx index b65536c2307d8..b265c4883323e 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx @@ -30,10 +30,10 @@ describe('getUrlTemplateWithMeta', () => { }); }); -describe('canFormatFeatureProperties', () => { +describe('hasTooltipProperties', () => { it('false if no tooltips', async () => { const source = new MVTSingleLayerVectorSource(descriptor); - expect(source.canFormatFeatureProperties()).toEqual(false); + expect(source.hasTooltipProperties()).toEqual(false); }); it('true if tooltip', async () => { const descriptorWithTooltips = { @@ -42,7 +42,7 @@ describe('canFormatFeatureProperties', () => { tooltipProperties: ['foobar'], }; const source = new MVTSingleLayerVectorSource(descriptorWithTooltips); - expect(source.canFormatFeatureProperties()).toEqual(true); + expect(source.hasTooltipProperties()).toEqual(true); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx index 92b643643ba2a..692e1fd18efaf 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -169,7 +169,7 @@ export class MVTSingleLayerVectorSource return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; } - canFormatFeatureProperties(): boolean { + hasTooltipProperties(): boolean { return !!this._tooltipFields.length; } diff --git a/x-pack/plugins/maps/public/classes/sources/table_source/table_source.ts b/x-pack/plugins/maps/public/classes/sources/table_source/table_source.ts index d4c7a7474c57c..372fb4983d7cc 100644 --- a/x-pack/plugins/maps/public/classes/sources/table_source/table_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/table_source/table_source.ts @@ -143,7 +143,7 @@ export class TableSource extends AbstractVectorSource implements ITermJoinSource }); } - canFormatFeatureProperties(): boolean { + hasTooltipProperties(): boolean { return false; } diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index b28cd7365d69e..da5a236a20936 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -60,7 +60,7 @@ export interface IVectorSource extends ISource { getSyncMeta(): VectorSourceSyncMeta | null; getFieldNames(): string[]; createField({ fieldName }: { fieldName: string }): IField; - canFormatFeatureProperties(): boolean; + hasTooltipProperties(): boolean; getSupportedShapeTypes(): Promise; isBoundsAware(): boolean; getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig; @@ -115,7 +115,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc throw new Error('Should implement VectorSource#getGeoJson'); } - canFormatFeatureProperties() { + hasTooltipProperties() { return false; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.tsx index 41a2b98ab4b28..f85b1c5de3619 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.tsx @@ -8,26 +8,21 @@ import React, { Component, Fragment, ReactNode } from 'react'; import { EuiIcon, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; import { GeoJsonProperties, Geometry } from 'geojson'; import { Filter } from 'src/plugins/data/public'; import { FeatureProperties } from './feature_properties'; -import { GEO_JSON_TYPE, ES_GEO_FIELD_TYPE, RawValue } from '../../../../common/constants'; -import { FeatureGeometryFilterForm } from './feature_geometry_filter_form'; +import { RawValue } from '../../../../common/constants'; import { Footer } from './footer'; import { Header } from './header'; -import { PreIndexedShape } from '../../../../common/elasticsearch_util'; -import { GeoFieldWithIndex } from '../../../components/geo_field_with_index'; -import { TooltipFeature } from '../../../../common/descriptor_types'; +import { GEOMETRY_FILTER_ACTION, TooltipFeature } from '../../../../common/descriptor_types'; import { ITooltipProperty } from '../../../classes/tooltips/tooltip_property'; import { ILayer } from '../../../classes/layers/layer'; -enum VIEWS { - PROPERTIES_VIEW = 'PROPERTIES_VIEW', - GEOMETRY_FILTER_VIEW = 'GEOMETRY_FILTER_VIEW', - FILTER_ACTIONS_VIEW = 'FILTER_ACTIONS_VIEW', -} +const PROPERTIES_VIEW = 'PROPERTIES_VIEW'; +const FILTER_ACTIONS_VIEW = 'FILTER_ACTIONS_VIEW'; + +type VIEWS = typeof PROPERTIES_VIEW | typeof FILTER_ACTIONS_VIEW | typeof GEOMETRY_FILTER_ACTION; interface Props { addFilters: ((filters: Filter[], actionId: string) => Promise) | null; @@ -55,14 +50,6 @@ interface Props { }) => Geometry | null; getLayerName: (layerId: string) => Promise; findLayerById: (layerId: string) => ILayer | undefined; - geoFields: GeoFieldWithIndex[]; - loadPreIndexedShape: ({ - layerId, - featureId, - }: { - layerId: string; - featureId?: string | number; - }) => Promise; } interface State { @@ -77,14 +64,14 @@ export class FeaturesTooltip extends Component { currentFeature: null, filterView: null, prevFeatures: [], - view: VIEWS.PROPERTIES_VIEW, + view: PROPERTIES_VIEW, }; static getDerivedStateFromProps(nextProps: Props, prevState: State) { if (nextProps.features !== prevState.prevFeatures) { return { currentFeature: nextProps.features ? nextProps.features[0] : null, - view: VIEWS.PROPERTIES_VIEW, + view: PROPERTIES_VIEW, prevFeatures: nextProps.features, }; } @@ -96,69 +83,37 @@ export class FeaturesTooltip extends Component { this.setState({ currentFeature: feature }); }; - _showGeometryFilterView = () => { - this.setState({ view: VIEWS.GEOMETRY_FILTER_VIEW }); - }; - _showPropertiesView = () => { - this.setState({ view: VIEWS.PROPERTIES_VIEW, filterView: null }); + this.setState({ view: PROPERTIES_VIEW, filterView: null }); }; _showFilterActionsView = (filterView: ReactNode) => { - this.setState({ view: VIEWS.FILTER_ACTIONS_VIEW, filterView }); + this.setState({ view: FILTER_ACTIONS_VIEW, filterView }); }; - _renderActions(geoFields: GeoFieldWithIndex[]) { - if (!this.props.isLocked || geoFields.length === 0) { - return null; - } - - return ( - - - - ); - } - - _filterGeoFields(featureGeometry: Geometry | null) { - if (!featureGeometry) { - return []; - } - - // line geometry can only create filters for geo_shape fields. + _renderActions() { if ( - featureGeometry.type === GEO_JSON_TYPE.LINE_STRING || - featureGeometry.type === GEO_JSON_TYPE.MULTI_LINE_STRING + !this.props.isLocked || + !this.state.currentFeature || + this.state.currentFeature.actions.length === 0 ) { - return this.props.geoFields.filter(({ geoFieldType }) => { - return geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE; - }); - } - - // TODO support geo distance filters for points - if ( - featureGeometry.type === GEO_JSON_TYPE.POINT || - featureGeometry.type === GEO_JSON_TYPE.MULTI_POINT - ) { - return []; - } - - return this.props.geoFields; - } - - _loadCurrentFeaturePreIndexedShape = async () => { - if (!this.state.currentFeature) { return null; } - return this.props.loadPreIndexedShape({ - layerId: this.state.currentFeature.layerId, - featureId: this.state.currentFeature.id, + return this.state.currentFeature.actions.map((action) => { + return ( + { + this.setState({ view: action.id }); + }} + key={action.id} + > + {action.label} + + ); }); - }; + } _renderBackButton(label: string) { return ( @@ -181,38 +136,20 @@ export class FeaturesTooltip extends Component { return null; } - const currentFeatureGeometry = this.props.loadFeatureGeometry({ - layerId: this.state.currentFeature.layerId, - featureId: this.state.currentFeature.id, + const action = this.state.currentFeature.actions.find(({ id }) => { + return id === this.state.view; }); - const geoFields = this._filterGeoFields(currentFeatureGeometry); - if ( - this.state.view === VIEWS.GEOMETRY_FILTER_VIEW && - currentFeatureGeometry && - this.props.addFilters - ) { + if (action) { return ( - {this._renderBackButton( - i18n.translate('xpack.maps.tooltip.showGeometryFilterViewLinkLabel', { - defaultMessage: 'Filter by geometry', - }) - )} - + {this._renderBackButton(action.label)} + {action.form} ); } - if (this.state.view === VIEWS.FILTER_ACTIONS_VIEW) { + if (this.state.view === FILTER_ACTIONS_VIEW) { return ( {this._renderBackButton( @@ -247,7 +184,7 @@ export class FeaturesTooltip extends Component { onSingleValueTrigger={this.props.onSingleValueTrigger} showFilterActions={this._showFilterActionsView} /> - {this._renderActions(geoFields)} + {this._renderActions()}