From d06104ef6cb0f13c8255d13a25f2ea3971fc9f2b Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 20 Jan 2022 15:30:28 +0100 Subject: [PATCH 01/43] Unskip search session api tests (#123158) --- .../api_integration/apis/search/session.ts | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/x-pack/test/api_integration/apis/search/session.ts b/x-pack/test/api_integration/apis/search/session.ts index 868c91cd9ed12..2c8a03ddbc4f2 100644 --- a/x-pack/test/api_integration/apis/search/session.ts +++ b/x-pack/test/api_integration/apis/search/session.ts @@ -16,8 +16,7 @@ export default function ({ getService }: FtrProviderContext) { const retry = getService('retry'); const spacesService = getService('spaces'); - // FLAKY: https://github.com/elastic/kibana/issues/119660 - describe.skip('search session', () => { + describe('search session', () => { describe('session management', () => { it('should fail to create a session with no name', async () => { const sessionId = `my-session-${Math.random()}`; @@ -189,7 +188,7 @@ export default function ({ getService }: FtrProviderContext) { const { id: id2 } = searchRes2.body; - await retry.waitForWithTimeout('searches persisted into session', 5000, async () => { + await retry.waitFor('searches persisted into session', async () => { const resp = await supertest .get(`/internal/session/${sessionId}`) .set('kbn-xsrf', 'foo') @@ -285,7 +284,7 @@ export default function ({ getService }: FtrProviderContext) { const { id: id2 } = searchRes2.body; - await retry.waitForWithTimeout('searches persisted into session', 5000, async () => { + await retry.waitFor('searches persisted into session', async () => { const resp = await supertest .get(`/internal/session/${sessionId}`) .set('kbn-xsrf', 'foo') @@ -342,7 +341,7 @@ export default function ({ getService }: FtrProviderContext) { }) .expect(200); - await retry.waitForWithTimeout('searches persisted into session', 5000, async () => { + await retry.waitFor('searches persisted into session', async () => { const resp = await supertest .get(`/internal/session/${sessionId}`) .set('kbn-xsrf', 'foo') @@ -361,9 +360,8 @@ export default function ({ getService }: FtrProviderContext) { // session refresh interval is 10 seconds, wait to give a chance for status to update await new Promise((resolve) => setTimeout(resolve, 10000)); - await retry.waitForWithTimeout( + await retry.waitFor( 'searches eventually complete and session gets into the complete state', - 5000, async () => { const resp = await supertest .get(`/internal/session/${sessionId}`) @@ -428,17 +426,21 @@ export default function ({ getService }: FtrProviderContext) { // it might take the session a moment to be updated await new Promise((resolve) => setTimeout(resolve, 2500)); - const getSessionSecondTime = await supertest - .get(`/internal/session/${sessionId}`) - .set('kbn-xsrf', 'foo') - .expect(200); + await retry.waitFor('search session touched time updated', async () => { + const getSessionSecondTime = await supertest + .get(`/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo') + .expect(200); - expect(getSessionFirstTime.body.attributes.sessionId).to.be.equal( - getSessionSecondTime.body.attributes.sessionId - ); - expect(getSessionFirstTime.body.attributes.touched).to.be.lessThan( - getSessionSecondTime.body.attributes.touched - ); + expect(getSessionFirstTime.body.attributes.sessionId).to.be.equal( + getSessionSecondTime.body.attributes.sessionId + ); + expect(getSessionFirstTime.body.attributes.touched).to.be.lessThan( + getSessionSecondTime.body.attributes.touched + ); + + return true; + }); }); describe('with security', () => { @@ -645,7 +647,7 @@ export default function ({ getService }: FtrProviderContext) { const { id } = searchRes.body; - await retry.waitForWithTimeout('searches persisted into session', 5000, async () => { + await retry.waitFor('searches persisted into session', async () => { const resp = await supertest .get(`/s/${spaceId}/internal/session/${sessionId}`) .set('kbn-xsrf', 'foo') @@ -682,7 +684,7 @@ export default function ({ getService }: FtrProviderContext) { ); }); - it('should complete persisten session', async () => { + it('should complete persisted session', async () => { const sessionId = `my-session-${Math.random()}`; // run search @@ -719,7 +721,7 @@ export default function ({ getService }: FtrProviderContext) { }) .expect(200); - await retry.waitForWithTimeout('searches persisted into session', 5000, async () => { + await retry.waitFor('searches persisted into session', async () => { const resp = await supertest .get(`/s/${spaceId}/internal/session/${sessionId}`) .set('kbn-xsrf', 'foo') @@ -738,9 +740,8 @@ export default function ({ getService }: FtrProviderContext) { // session refresh interval is 5 seconds, wait to give a chance for status to update await new Promise((resolve) => setTimeout(resolve, 5000)); - await retry.waitForWithTimeout( + await retry.waitFor( 'searches eventually complete and session gets into the complete state', - 5000, async () => { const resp = await supertest .get(`/s/${spaceId}/internal/session/${sessionId}`) From 460f89a200751d01da4159c80af87bbff540b673 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 20 Jan 2022 16:05:04 +0100 Subject: [PATCH 02/43] [bfetch] Improve error handling (#123455) --- .../public/streaming/fetch_streaming.test.ts | 1 + .../streaming/from_streaming_xhr.test.ts | 32 ++++++++++++++++++- .../public/streaming/from_streaming_xhr.ts | 7 ++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts b/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts index 67ebf8d5a1c23..21afa03c18920 100644 --- a/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts +++ b/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts @@ -22,6 +22,7 @@ const tick = () => new Promise((resolve) => setTimeout(resolve, 1)); const setup = () => { const { xhr, XMLHttpRequest } = mockXMLHttpRequest(); window.XMLHttpRequest = XMLHttpRequest; + (xhr as any).status = 200; return { xhr }; }; diff --git a/src/plugins/bfetch/public/streaming/from_streaming_xhr.test.ts b/src/plugins/bfetch/public/streaming/from_streaming_xhr.test.ts index bc5bf28183a50..d73f7a9b5099b 100644 --- a/src/plugins/bfetch/public/streaming/from_streaming_xhr.test.ts +++ b/src/plugins/bfetch/public/streaming/from_streaming_xhr.test.ts @@ -15,7 +15,7 @@ const createXhr = (): XMLHttpRequest => onreadystatechange: () => {}, readyState: 0, responseText: '', - status: 0, + status: 200, } as unknown as XMLHttpRequest); test('returns observable', () => { @@ -156,6 +156,36 @@ test('errors observable if request returns with error', () => { ); }); +test('does not emit when gets error response', () => { + const xhr = createXhr(); + const observable = fromStreamingXhr(xhr); + + const next = jest.fn(); + const complete = jest.fn(); + const error = jest.fn(); + observable.subscribe({ + next, + complete, + error, + }); + + (xhr as any).responseText = 'error'; + (xhr as any).status = 400; + xhr.onprogress!({} as any); + + expect(next).toHaveBeenCalledTimes(0); + + (xhr as any).readyState = 4; + xhr.onreadystatechange!({} as any); + + expect(next).toHaveBeenCalledTimes(0); + expect(error).toHaveBeenCalledTimes(1); + expect(error.mock.calls[0][0]).toBeInstanceOf(Error); + expect(error.mock.calls[0][0].message).toMatchInlineSnapshot( + `"Batch request failed with status 400"` + ); +}); + test('when .onprogress called multiple times with same text, does not create new observable events', () => { const xhr = createXhr(); const observable = fromStreamingXhr(xhr); diff --git a/src/plugins/bfetch/public/streaming/from_streaming_xhr.ts b/src/plugins/bfetch/public/streaming/from_streaming_xhr.ts index c630554c73fa0..d81c7bfa694c2 100644 --- a/src/plugins/bfetch/public/streaming/from_streaming_xhr.ts +++ b/src/plugins/bfetch/public/streaming/from_streaming_xhr.ts @@ -23,8 +23,12 @@ export const fromStreamingXhr = ( let index = 0; let aborted = false; + // 0 indicates a network failure. 400+ messages are considered server errors + const isErrorStatus = () => xhr.status === 0 || xhr.status >= 400; + const processBatch = () => { if (aborted) return; + if (isErrorStatus()) return; const { responseText } = xhr; if (index >= responseText.length) return; @@ -56,8 +60,7 @@ export const fromStreamingXhr = ( if (xhr.readyState === 4) { if (signal) signal.removeEventListener('abort', onBatchAbort); - // 0 indicates a network failure. 400+ messages are considered server errors - if (xhr.status === 0 || xhr.status >= 400) { + if (isErrorStatus()) { subject.error(new Error(`Batch request failed with status ${xhr.status}`)); } else { subject.complete(); From b1d7731afaa2a1e4e86296364da6f4e795c9975a Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 20 Jan 2022 08:33:14 -0700 Subject: [PATCH 03/43] [Dashboard] Clone ReferenceOrValueEmbeddables by value. (#122199) * Clone all panels by value. * Moved removal of byReference properties to getInputAsValueType. * Fixed handling of clone titles. * Fixed functional and unit clone tests. * Removed duplicate check for byReference. * Unset title on Visualize embeddable when by value. * Remove unused import. * Added by reference logic for saved search embeddables. * Re-added unit tests for cloning by reference. * Added functional tests. * Added Jest unit tests. * Ignored TypeScript errors for calling private functions in Jest tests. * Adjusted logic for generating clone titles. * Edited unit and functional tests for clone titles. * Fixed typo in Jest tests. * Keep hidden panel title status. * Fix Jest test description. * Remove unused import. * Fixed Jest tests after new title logic. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../actions/clone_panel_action.test.tsx | 192 ++++++++++++++---- .../actions/clone_panel_action.tsx | 123 +++++++---- .../actions/unlink_from_library_action.tsx | 3 - .../embeddable/dashboard_container.tsx | 14 ++ .../attribute_service/attribute_service.tsx | 6 +- .../embeddable/visualize_embeddable.tsx | 4 +- .../apps/dashboard/panel_cloning.ts | 21 ++ 7 files changed, 286 insertions(+), 77 deletions(-) diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx index 3c3872226ffb0..84c968e1c2b24 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx @@ -33,7 +33,8 @@ setup.registerEmbeddableFactory( const start = doStart(); let container: DashboardContainer; -let embeddable: ContactCardEmbeddable; +let byRefOrValEmbeddable: ContactCardEmbeddable; +let genericEmbeddable: ContactCardEmbeddable; let coreStart: CoreStart; beforeEach(async () => { coreStart = coreMock.createStart(); @@ -70,18 +71,38 @@ beforeEach(async () => { }); container = new DashboardContainer(input, options); - const contactCardEmbeddable = await container.addNewEmbeddable< + const refOrValContactCardEmbeddable = await container.addNewEmbeddable< ContactCardEmbeddableInput, ContactCardEmbeddableOutput, ContactCardEmbeddable >(CONTACT_CARD_EMBEDDABLE, { - firstName: 'Kibana', + firstName: 'RefOrValEmbeddable', + }); + const genericContactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'NotRefOrValEmbeddable', }); - if (isErrorEmbeddable(contactCardEmbeddable)) { - throw new Error('Failed to create embeddable'); + if ( + isErrorEmbeddable(refOrValContactCardEmbeddable) || + isErrorEmbeddable(genericContactCardEmbeddable) + ) { + throw new Error('Failed to create embeddables'); } else { - embeddable = contactCardEmbeddable; + byRefOrValEmbeddable = embeddablePluginMock.mockRefOrValEmbeddable< + ContactCardEmbeddable, + ContactCardEmbeddableInput + >(refOrValContactCardEmbeddable, { + mockedByReferenceInput: { + savedObjectId: 'testSavedObjectId', + id: refOrValContactCardEmbeddable.id, + }, + mockedByValueInput: { firstName: 'Kibanana', id: refOrValContactCardEmbeddable.id }, + }); + genericEmbeddable = genericContactCardEmbeddable; } }); @@ -90,17 +111,17 @@ test('Clone is incompatible with Error Embeddables', async () => { const errorEmbeddable = new ErrorEmbeddable( 'Wow what an awful error', { id: ' 404' }, - embeddable.getRoot() as IContainer + byRefOrValEmbeddable.getRoot() as IContainer ); expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); }); test('Clone adds a new embeddable', async () => { - const dashboard = embeddable.getRoot() as IContainer; + const dashboard = byRefOrValEmbeddable.getRoot() as IContainer; const originalPanelCount = Object.keys(dashboard.getInput().panels).length; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); const action = new ClonePanelAction(coreStart); - await action.execute({ embeddable }); + await action.execute({ embeddable: byRefOrValEmbeddable }); expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount + 1); const newPanelId = Object.keys(container.getInput().panels).find( (key) => !originalPanelKeySet.has(key) @@ -113,56 +134,159 @@ test('Clone adds a new embeddable', async () => { await new Promise((r) => process.nextTick(r)); // Allow the current loop of the event loop to run to completion // now wait for the full embeddable to replace it const loadedPanel = await dashboard.untilEmbeddableLoaded(newPanelId!); - expect(loadedPanel.type).toEqual(embeddable.type); + expect(loadedPanel.type).toEqual(byRefOrValEmbeddable.type); }); -test('Clones an embeddable without a saved object ID', async () => { - const dashboard = embeddable.getRoot() as IContainer; - const panel = dashboard.getInput().panels[embeddable.id] as DashboardPanelState; +test('Clones a RefOrVal embeddable by value', async () => { + const dashboard = byRefOrValEmbeddable.getRoot() as IContainer; + const panel = dashboard.getInput().panels[byRefOrValEmbeddable.id] as DashboardPanelState; const action = new ClonePanelAction(coreStart); // @ts-ignore - const newPanel = await action.cloneEmbeddable(panel, embeddable.type); - expect(newPanel.type).toEqual(embeddable.type); + const newPanel = await action.cloneEmbeddable(panel, byRefOrValEmbeddable); + expect(coreStart.savedObjects.client.get).toHaveBeenCalledTimes(0); + expect(coreStart.savedObjects.client.find).toHaveBeenCalledTimes(0); + expect(coreStart.savedObjects.client.create).toHaveBeenCalledTimes(0); + expect(newPanel.type).toEqual(byRefOrValEmbeddable.type); }); -test('Clones an embeddable with a saved object ID', async () => { - const dashboard = embeddable.getRoot() as IContainer; - const panel = dashboard.getInput().panels[embeddable.id] as DashboardPanelState; +test('Clones a non-RefOrVal embeddable by value if the panel does not have a savedObjectId', async () => { + const dashboard = genericEmbeddable.getRoot() as IContainer; + const panel = dashboard.getInput().panels[genericEmbeddable.id] as DashboardPanelState; + const action = new ClonePanelAction(coreStart); + // @ts-ignore + const newPanelWithoutId = await action.cloneEmbeddable(panel, genericEmbeddable); + expect(coreStart.savedObjects.client.get).toHaveBeenCalledTimes(0); + expect(coreStart.savedObjects.client.find).toHaveBeenCalledTimes(0); + expect(coreStart.savedObjects.client.create).toHaveBeenCalledTimes(0); + expect(newPanelWithoutId.type).toEqual(genericEmbeddable.type); +}); + +test('Clones a non-RefOrVal embeddable by reference if the panel has a savedObjectId', async () => { + const dashboard = genericEmbeddable.getRoot() as IContainer; + const panel = dashboard.getInput().panels[genericEmbeddable.id] as DashboardPanelState; panel.explicitInput.savedObjectId = 'holySavedObjectBatman'; const action = new ClonePanelAction(coreStart); // @ts-ignore - const newPanel = await action.cloneEmbeddable(panel, embeddable.type); + const newPanel = await action.cloneEmbeddable(panel, genericEmbeddable); expect(coreStart.savedObjects.client.get).toHaveBeenCalledTimes(1); expect(coreStart.savedObjects.client.find).toHaveBeenCalledTimes(1); expect(coreStart.savedObjects.client.create).toHaveBeenCalledTimes(1); - expect(newPanel.type).toEqual(embeddable.type); + expect(newPanel.type).toEqual(genericEmbeddable.type); }); -test('Gets a unique title ', async () => { +test('Gets a unique title from the saved objects library', async () => { + const dashboard = genericEmbeddable.getRoot() as IContainer; + const panel = dashboard.getInput().panels[genericEmbeddable.id] as DashboardPanelState; + panel.explicitInput.savedObjectId = 'holySavedObjectBatman'; coreStart.savedObjects.client.find = jest.fn().mockImplementation(({ search }) => { - if (search === '"testFirstTitle"') return { total: 1 }; - else if (search === '"testSecondTitle"') return { total: 41 }; - else if (search === '"testThirdTitle"') return { total: 90 }; + if (search === '"testFirstClone"') { + return { + savedObjects: [ + { + attributes: { title: 'testFirstClone' }, + get: jest.fn().mockReturnValue('testFirstClone'), + }, + ], + total: 1, + }; + } else if (search === '"testBeforePageLimit"') { + return { + savedObjects: [ + { + attributes: { title: 'testBeforePageLimit (copy 9)' }, + get: jest.fn().mockReturnValue('testBeforePageLimit (copy 9)'), + }, + ], + total: 10, + }; + } else if (search === '"testMaxLogic"') { + return { + savedObjects: [ + { + attributes: { title: 'testMaxLogic (copy 10000)' }, + get: jest.fn().mockReturnValue('testMaxLogic (copy 10000)'), + }, + ], + total: 2, + }; + } else if (search === '"testAfterPageLimit"') { + return { total: 11 }; + } }); + + const action = new ClonePanelAction(coreStart); + // @ts-ignore + expect(await action.getCloneTitle(genericEmbeddable, 'testFirstClone')).toEqual( + 'testFirstClone (copy)' + ); + // @ts-ignore + expect(await action.getCloneTitle(genericEmbeddable, 'testBeforePageLimit')).toEqual( + 'testBeforePageLimit (copy 10)' + ); + // @ts-ignore + expect(await action.getCloneTitle(genericEmbeddable, 'testBeforePageLimit (copy 9)')).toEqual( + 'testBeforePageLimit (copy 10)' + ); + // @ts-ignore + expect(await action.getCloneTitle(genericEmbeddable, 'testMaxLogic')).toEqual( + 'testMaxLogic (copy 10001)' + ); + // @ts-ignore + expect(await action.getCloneTitle(genericEmbeddable, 'testAfterPageLimit')).toEqual( + 'testAfterPageLimit (copy 11)' + ); + // @ts-ignore + expect(await action.getCloneTitle(genericEmbeddable, 'testAfterPageLimit (copy 10)')).toEqual( + 'testAfterPageLimit (copy 11)' + ); + // @ts-ignore + expect(await action.getCloneTitle(genericEmbeddable, 'testAfterPageLimit (copy 10000)')).toEqual( + 'testAfterPageLimit (copy 11)' + ); +}); + +test('Gets a unique title from the dashboard', async () => { + const dashboard = genericEmbeddable.getRoot() as DashboardContainer; const action = new ClonePanelAction(coreStart); + // @ts-ignore - expect(await action.getUniqueTitle('testFirstTitle', embeddable.type)).toEqual( - 'testFirstTitle (copy)' + expect(await action.getCloneTitle(byRefOrValEmbeddable, '')).toEqual(''); + + dashboard.getPanelTitles = jest.fn().mockImplementation(() => { + return ['testDuplicateTitle', 'testDuplicateTitle (copy)', 'testUniqueTitle']; + }); + // @ts-ignore + expect(await action.getCloneTitle(byRefOrValEmbeddable, 'testUniqueTitle')).toEqual( + 'testUniqueTitle (copy)' ); // @ts-ignore - expect(await action.getUniqueTitle('testSecondTitle (copy 39)', embeddable.type)).toEqual( - 'testSecondTitle (copy 40)' + expect(await action.getCloneTitle(byRefOrValEmbeddable, 'testDuplicateTitle')).toEqual( + 'testDuplicateTitle (copy 1)' ); + + dashboard.getPanelTitles = jest.fn().mockImplementation(() => { + return ['testDuplicateTitle', 'testDuplicateTitle (copy)'].concat( + Array.from([...Array(39)], (_, index) => `testDuplicateTitle (copy ${index + 1})`) + ); + }); // @ts-ignore - expect(await action.getUniqueTitle('testSecondTitle (copy 20)', embeddable.type)).toEqual( - 'testSecondTitle (copy 40)' + expect(await action.getCloneTitle(byRefOrValEmbeddable, 'testDuplicateTitle')).toEqual( + 'testDuplicateTitle (copy 40)' ); // @ts-ignore - expect(await action.getUniqueTitle('testThirdTitle', embeddable.type)).toEqual( - 'testThirdTitle (copy 89)' + expect(await action.getCloneTitle(byRefOrValEmbeddable, 'testDuplicateTitle (copy 100)')).toEqual( + 'testDuplicateTitle (copy 40)' + ); + + dashboard.getPanelTitles = jest.fn().mockImplementation(() => { + return ['testDuplicateTitle (copy 100)']; + }); + // @ts-ignore + expect(await action.getCloneTitle(byRefOrValEmbeddable, 'testDuplicateTitle')).toEqual( + 'testDuplicateTitle (copy 101)' ); // @ts-ignore - expect(await action.getUniqueTitle('testThirdTitle (copy 10000)', embeddable.type)).toEqual( - 'testThirdTitle (copy 89)' + expect(await action.getCloneTitle(byRefOrValEmbeddable, 'testDuplicateTitle (copy 100)')).toEqual( + 'testDuplicateTitle (copy 101)' ); }); diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx index 829344504b16b..4b2e1e39818ad 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -20,6 +20,7 @@ import { EmbeddableInput, SavedObjectEmbeddableInput, isErrorEmbeddable, + isReferenceOrValueEmbeddable, } from '../../services/embeddable'; import { placePanelBeside, @@ -78,7 +79,7 @@ export class ClonePanelAction implements Action { } dashboard.showPlaceholderUntil( - this.cloneEmbeddable(panelToClone, embeddable.type), + this.cloneEmbeddable(panelToClone, embeddable), placePanelBeside, { width: panelToClone.gridData.w, @@ -89,56 +90,106 @@ export class ClonePanelAction implements Action { ); } - private async getUniqueTitle(rawTitle: string, embeddableType: string): Promise { + private async getCloneTitle(embeddable: IEmbeddable, rawTitle: string) { + if (rawTitle === '') return ''; // If + const clonedTag = dashboardClonePanelAction.getClonedTag(); const cloneRegex = new RegExp(`\\(${clonedTag}\\)`, 'g'); const cloneNumberRegex = new RegExp(`\\(${clonedTag} [0-9]+\\)`, 'g'); const baseTitle = rawTitle.replace(cloneNumberRegex, '').replace(cloneRegex, '').trim(); + let similarTitles: string[]; + if ( + isReferenceOrValueEmbeddable(embeddable) || + !_.has(embeddable.getExplicitInput(), 'savedObjectId') + ) { + const dashboard: DashboardContainer = embeddable.getRoot() as DashboardContainer; + similarTitles = _.filter(await dashboard.getPanelTitles(), (title: string) => { + return title.startsWith(baseTitle); + }); + } else { + const perPage = 10; + const similarSavedObjects = await this.core.savedObjects.client.find({ + type: embeddable.type, + perPage, + fields: ['title'], + searchFields: ['title'], + search: `"${baseTitle}"`, + }); + if (similarSavedObjects.total <= perPage) { + similarTitles = similarSavedObjects.savedObjects.map((savedObject) => { + return savedObject.get('title'); + }); + } else { + similarTitles = [baseTitle + ` (${clonedTag} ${similarSavedObjects.total - 1})`]; + } + } - const similarSavedObjects = await this.core.savedObjects.client.find({ - type: embeddableType, - perPage: 0, - fields: ['title'], - searchFields: ['title'], - search: `"${baseTitle}"`, + const cloneNumbers = _.map(similarTitles, (title: string) => { + if (title.match(cloneRegex)) return 0; + const cloneTag = title.match(cloneNumberRegex); + return cloneTag ? parseInt(cloneTag[0].replace(/[^0-9.]/g, ''), 10) : -1; }); - const similarBaseTitlesCount: number = similarSavedObjects.total - 1; + const similarBaseTitlesCount = _.max(cloneNumbers) || 0; - return similarBaseTitlesCount <= 0 + return similarBaseTitlesCount < 0 ? baseTitle + ` (${clonedTag})` - : baseTitle + ` (${clonedTag} ${similarBaseTitlesCount})`; + : baseTitle + ` (${clonedTag} ${similarBaseTitlesCount + 1})`; + } + + private async addCloneToLibrary( + embeddable: IEmbeddable, + objectIdToClone: string + ): Promise { + const savedObjectToClone = await this.core.savedObjects.client.get( + embeddable.type, + objectIdToClone + ); + + // Clone the saved object + const newTitle = await this.getCloneTitle(embeddable, savedObjectToClone.attributes.title); + const clonedSavedObject = await this.core.savedObjects.client.create( + embeddable.type, + { + ..._.cloneDeep(savedObjectToClone.attributes), + title: newTitle, + }, + { references: _.cloneDeep(savedObjectToClone.references) } + ); + return clonedSavedObject.id; } private async cloneEmbeddable( panelToClone: DashboardPanelState, - embeddableType: string + embeddable: IEmbeddable ): Promise> { - const panelState: PanelState = { - type: embeddableType, - explicitInput: { - ...panelToClone.explicitInput, - id: uuid.v4(), - }, - }; - let newTitle: string = ''; - if (panelToClone.explicitInput.savedObjectId) { - // Fetch existing saved object - const savedObjectToClone = await this.core.savedObjects.client.get( - embeddableType, - panelToClone.explicitInput.savedObjectId - ); - - // Clone the saved object - newTitle = await this.getUniqueTitle(savedObjectToClone.attributes.title, embeddableType); - const clonedSavedObject = await this.core.savedObjects.client.create( - embeddableType, - { - ..._.cloneDeep(savedObjectToClone.attributes), + let panelState: PanelState; + if (isReferenceOrValueEmbeddable(embeddable)) { + const newTitle = await this.getCloneTitle(embeddable, embeddable.getTitle() || ''); + panelState = { + type: embeddable.type, + explicitInput: { + ...(await embeddable.getInputAsValueType()), + id: uuid.v4(), title: newTitle, + hidePanelTitles: panelToClone.explicitInput.hidePanelTitles, + }, + }; + } else { + panelState = { + type: embeddable.type, + explicitInput: { + ...panelToClone.explicitInput, + id: uuid.v4(), }, - { references: _.cloneDeep(savedObjectToClone.references) } - ); - (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId = clonedSavedObject.id; + }; + if (panelToClone.explicitInput.savedObjectId) { + const clonedSavedObjectId = await this.addCloneToLibrary( + embeddable, + panelToClone.explicitInput.savedObjectId + ); + (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId = + clonedSavedObjectId; + } } this.core.notifications.toasts.addSuccess({ title: dashboardClonePanelAction.getSuccessMessage(), diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx index 68a5950269517..4eae24bc892f7 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx @@ -79,9 +79,6 @@ export class UnlinkFromLibraryAction implements Action { + const titles: string[] = []; + const ids: string[] = Object.keys(this.getInput().panels); + for (const panelId of ids) { + await this.untilEmbeddableLoaded(panelId); + const child: IEmbeddable = this.getChild(panelId); + const title = child.getTitle(); + if (title) { + titles.push(title); + } + } + return titles; + } + constructor( initialInput: DashboardContainerInput, private readonly services: DashboardContainerServices, diff --git a/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx index 3826162aa6590..81b836796d31d 100644 --- a/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx +++ b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx @@ -137,10 +137,14 @@ export class AttributeService< return input as ValType; } const { attributes } = await this.unwrapAttributes(input); + const libraryTitle = attributes.title; const { savedObjectId, ...originalInputToPropagate } = input; + return { ...originalInputToPropagate, - attributes, + // by value visualizations should not have default titles and/or descriptions + ...{ attributes: omit(attributes, ['title', 'description']) }, + title: libraryTitle, } as unknown as ValType; }; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index 9c328a175c10b..c3b8834605d1d 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -455,10 +455,8 @@ export class VisualizeEmbeddable const input = { savedVis: this.vis.serialize(), }; - if (this.getTitle()) { - input.savedVis.title = this.getTitle(); - } delete input.savedVis.id; + _.unset(input, 'savedVis.title'); return new Promise((resolve) => { resolve({ ...(input as VisualizeByValueInput) }); }); diff --git a/test/functional/apps/dashboard/panel_cloning.ts b/test/functional/apps/dashboard/panel_cloning.ts index 1be9175e45ad9..a2cadd89f486a 100644 --- a/test/functional/apps/dashboard/panel_cloning.ts +++ b/test/functional/apps/dashboard/panel_cloning.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardAddPanel = getService('dashboardAddPanel'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects([ 'dashboard', 'header', @@ -53,6 +54,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(panelDimensions[0]).to.eql(panelDimensions[1]); }); + it('clone of a by reference embeddable is by value', async () => { + const panelName = PIE_CHART_VIS_NAME.replace(/\s+/g, ''); + const clonedPanel = await testSubjects.find(`embeddablePanelHeading-${panelName}(copy)`); + const descendants = await testSubjects.findAllDescendant( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + clonedPanel + ); + expect(descendants.length).to.equal(0); + }); + it('gives a correct title to the clone of a clone', async () => { const initialPanelTitles = await PageObjects.dashboard.getPanelTitles(); const clonedPanelName = initialPanelTitles[initialPanelTitles.length - 1]; @@ -65,5 +76,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { PIE_CHART_VIS_NAME + ' (copy 1)' ); }); + + it('clone of a by value embeddable is by value', async () => { + const panelName = PIE_CHART_VIS_NAME.replace(/\s+/g, ''); + const clonedPanel = await testSubjects.find(`embeddablePanelHeading-${panelName}(copy1)`); + const descendants = await testSubjects.findAllDescendant( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + clonedPanel + ); + expect(descendants.length).to.equal(0); + }); }); } From c5aa390f1ce2e9677886aa10dd0a73b7e0fde664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Thu, 20 Jan 2022 16:45:33 +0100 Subject: [PATCH 04/43] [Security Solution][Endpoint] Prevent event filter list to be created twice from policy details page. (#123350) * Adds new module with service actions to be used in the service class and in the hooks without using the class. Also marks the class as deprecated. * New service actions to be used in service class and hooks Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../pages/event_filters/service/index.ts | 108 +++---------- .../event_filters/service/service_actions.ts | 148 ++++++++++++++++++ ...policy_event_filters_delete_modal.test.tsx | 4 +- .../policy_event_filters_flyout.test.tsx | 4 +- .../pages/policy/view/event_filters/hooks.ts | 31 ++-- 5 files changed, 185 insertions(+), 110 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/event_filters/service/service_actions.ts diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts index e5c7a35d89c00..6145f869a660a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts @@ -13,36 +13,25 @@ import type { UpdateExceptionListItemSchema, ExceptionListSummarySchema, } from '@kbn/securitysolution-io-ts-list-types'; -import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { Immutable } from '../../../../../common/endpoint/types'; -import { EVENT_FILTER_LIST, EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../constants'; import { EventFiltersService } from '../types'; - +import { + addEventFilters, + getList, + getOne, + updateOne, + deleteOne, + getSummary, +} from './service_actions'; + +/** + * @deprecated Don't use this class for future implementations, use the service_actions module instead! + */ export class EventFiltersHttpService implements EventFiltersService { - private listHasBeenCreated: boolean; - constructor(private http: HttpStart) { - this.listHasBeenCreated = false; - } - - private async createEndpointEventList() { - try { - await this.http.post(EXCEPTION_LIST_URL, { - body: JSON.stringify(EVENT_FILTER_LIST), - }); - this.listHasBeenCreated = true; - } catch (err) { - // Ignore 409 errors. List already created - if (err.response.status === 409) this.listHasBeenCreated = true; - else throw err; - } - } - - private async httpWrapper() { - if (!this.listHasBeenCreated) await this.createEndpointEventList(); - return this.http; + this.http = http; } async getList({ @@ -58,87 +47,28 @@ export class EventFiltersHttpService implements EventFiltersService { sortOrder: string; filter: string; }> = {}): Promise { - const http = await this.httpWrapper(); - return http.get(`${EXCEPTION_LIST_ITEM_URL}/_find`, { - query: { - page, - per_page: perPage, - sort_field: sortField, - sort_order: sortOrder, - list_id: [ENDPOINT_EVENT_FILTERS_LIST_ID], - namespace_type: ['agnostic'], - filter, - }, - }); + return getList({ http: this.http, perPage, page, sortField, sortOrder, filter }); } async addEventFilters(exception: ExceptionListItemSchema | CreateExceptionListItemSchema) { - return (await this.httpWrapper()).post(EXCEPTION_LIST_ITEM_URL, { - body: JSON.stringify(exception), - }); + return addEventFilters(this.http, exception); } async getOne(id: string) { - return (await this.httpWrapper()).get(EXCEPTION_LIST_ITEM_URL, { - query: { - id, - namespace_type: 'agnostic', - }, - }); + return getOne(this.http, id); } async updateOne( exception: Immutable ): Promise { - return (await this.httpWrapper()).put(EXCEPTION_LIST_ITEM_URL, { - body: JSON.stringify(EventFiltersHttpService.cleanEventFilterToUpdate(exception)), - }); + return updateOne(this.http, exception); } async deleteOne(id: string): Promise { - return (await this.httpWrapper()).delete(EXCEPTION_LIST_ITEM_URL, { - query: { - id, - namespace_type: 'agnostic', - }, - }); + return deleteOne(this.http, id); } async getSummary(): Promise { - return (await this.httpWrapper()).get( - `${EXCEPTION_LIST_URL}/summary`, - { - query: { - list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, - namespace_type: 'agnostic', - }, - } - ); - } - - static cleanEventFilterToUpdate( - exception: Immutable - ): UpdateExceptionListItemSchema { - const exceptionToUpdateCleaned = { ...exception }; - // Clean unnecessary fields for update action - [ - 'created_at', - 'created_by', - 'created_at', - 'created_by', - 'list_id', - 'tie_breaker_id', - 'updated_at', - 'updated_by', - ].forEach((field) => { - delete exceptionToUpdateCleaned[field as keyof UpdateExceptionListItemSchema]; - }); - - exceptionToUpdateCleaned.comments = exceptionToUpdateCleaned.comments?.map((comment) => ({ - comment: comment.comment, - id: comment.id, - })); - - return exceptionToUpdateCleaned as UpdateExceptionListItemSchema; + return getSummary(this.http); } } diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/service_actions.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/service_actions.ts new file mode 100644 index 0000000000000..19c56cb5fed90 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/service_actions.ts @@ -0,0 +1,148 @@ +/* + * 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 { + CreateExceptionListItemSchema, + ExceptionListItemSchema, + ExceptionListSummarySchema, + FoundExceptionListItemSchema, + UpdateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { HttpStart } from 'kibana/public'; +import { + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_URL, + EVENT_FILTER_LIST, + ENDPOINT_EVENT_FILTERS_LIST_ID, +} from '../constants'; +import { Immutable } from '../../../../../common/endpoint/types'; + +async function createEventFilterList(http: HttpStart): Promise { + try { + await http.post(EXCEPTION_LIST_URL, { + body: JSON.stringify(EVENT_FILTER_LIST), + }); + } catch (err) { + // Ignore 409 errors. List already created + if (err.response?.status !== 409) { + throw err; + } + } +} + +let listExistsPromise: Promise; +export async function ensureEventFiltersListExists(http: HttpStart): Promise { + if (!listExistsPromise) { + listExistsPromise = createEventFilterList(http); + } + await listExistsPromise; +} + +export async function getList({ + http, + perPage, + page, + sortField, + sortOrder, + filter, +}: { + http: HttpStart; + page?: number; + perPage?: number; + sortField?: string; + sortOrder?: string; + filter?: string; +}): Promise { + await ensureEventFiltersListExists(http); + return http.get(`${EXCEPTION_LIST_ITEM_URL}/_find`, { + query: { + page, + per_page: perPage, + sort_field: sortField, + sort_order: sortOrder, + list_id: [ENDPOINT_EVENT_FILTERS_LIST_ID], + namespace_type: ['agnostic'], + filter, + }, + }); +} + +export async function addEventFilters( + http: HttpStart, + exception: ExceptionListItemSchema | CreateExceptionListItemSchema +) { + await ensureEventFiltersListExists(http); + return http.post(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(exception), + }); +} + +export async function getOne(http: HttpStart, id: string) { + await ensureEventFiltersListExists(http); + return http.get(EXCEPTION_LIST_ITEM_URL, { + query: { + id, + namespace_type: 'agnostic', + }, + }); +} + +export async function updateOne( + http: HttpStart, + exception: Immutable +): Promise { + await ensureEventFiltersListExists(http); + return http.put(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(cleanEventFilterToUpdate(exception)), + }); +} + +export async function deleteOne(http: HttpStart, id: string): Promise { + await ensureEventFiltersListExists(http); + return http.delete(EXCEPTION_LIST_ITEM_URL, { + query: { + id, + namespace_type: 'agnostic', + }, + }); +} + +export async function getSummary(http: HttpStart): Promise { + await ensureEventFiltersListExists(http); + return http.get(`${EXCEPTION_LIST_URL}/summary`, { + query: { + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + namespace_type: 'agnostic', + }, + }); +} + +export function cleanEventFilterToUpdate( + exception: Immutable +): UpdateExceptionListItemSchema { + const exceptionToUpdateCleaned = { ...exception }; + // Clean unnecessary fields for update action + [ + 'created_at', + 'created_by', + 'created_at', + 'created_by', + 'list_id', + 'tie_breaker_id', + 'updated_at', + 'updated_by', + ].forEach((field) => { + delete exceptionToUpdateCleaned[field as keyof UpdateExceptionListItemSchema]; + }); + + exceptionToUpdateCleaned.comments = exceptionToUpdateCleaned.comments?.map((comment) => ({ + comment: comment.comment, + id: comment.id, + })); + + return exceptionToUpdateCleaned as UpdateExceptionListItemSchema; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.test.tsx index 6711b48326bbf..20522e35e8983 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.test.tsx @@ -17,7 +17,7 @@ import { } from '../../../../../../common/mock/endpoint'; import { PolicyEventFiltersDeleteModal } from './policy_event_filters_delete_modal'; import { eventFiltersListQueryHttpMock } from '../../../../event_filters/test_utils'; -import { EventFiltersHttpService } from '../../../../event_filters/service'; +import { cleanEventFilterToUpdate } from '../../../../event_filters/service/service_actions'; describe('Policy details event filter delete modal', () => { let policyId: string; @@ -73,7 +73,7 @@ describe('Policy details event filter delete modal', () => { await waitFor(() => { expect(mockedApi.responseProvider.eventFiltersUpdateOne).toHaveBeenLastCalledWith({ body: JSON.stringify( - EventFiltersHttpService.cleanEventFilterToUpdate({ + cleanEventFilterToUpdate({ ...exception, tags: ['policy:1234', 'policy:4321', 'not-a-policy-tag'], }) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx index ef1cbc8163705..7d984cdb2a382 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx @@ -25,7 +25,7 @@ import { FoundExceptionListItemSchema, UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; -import { EventFiltersHttpService } from '../../../../event_filters/service'; +import { cleanEventFilterToUpdate } from '../../../../event_filters/service/service_actions'; const getDefaultQueryParameters = (customFilter: string | undefined = '') => ({ path: '/api/exception_lists/items/_find', @@ -56,7 +56,7 @@ const getCleanedExceptionWithNewTags = ( tags: [...testTags, `policy:${policy.id}`], }; - return EventFiltersHttpService.cleanEventFilterToUpdate(exceptionToUpdateWithNewTags); + return cleanEventFilterToUpdate(exceptionToUpdateWithNewTags); }; describe('Policy details event filters flyout', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts index fd5e00668c164..24e3cb464d4d3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useMemo } from 'react'; import pMap from 'p-map'; import { ExceptionListItemSchema, @@ -13,24 +12,20 @@ import { import { QueryObserverResult, useMutation, useQuery, useQueryClient } from 'react-query'; import { ServerApiError } from '../../../../../common/types'; import { useHttp } from '../../../../../common/lib/kibana'; -import { EventFiltersHttpService } from '../../../event_filters/service'; +import { getList, updateOne } from '../../../event_filters/service/service_actions'; import { parseQueryFilterToKQL, parsePoliciesAndFilterToKql } from '../../../../common/utils'; import { SEARCHABLE_FIELDS } from '../../../event_filters/constants'; -export function useGetEventFiltersService() { - const http = useHttp(); - return useMemo(() => new EventFiltersHttpService(http), [http]); -} - export function useGetAllAssignedEventFilters( policyId: string, enabled: boolean = true ): QueryObserverResult { - const service = useGetEventFiltersService(); + const http = useHttp(); return useQuery( ['eventFilters', 'assigned', policyId], () => { - return service.getList({ + return getList({ + http, filter: parsePoliciesAndFilterToKql({ policies: [...(policyId ? [policyId] : []), 'all'] }), }); }, @@ -48,13 +43,14 @@ export function useSearchAssignedEventFilters( policyId: string, options: { filter?: string; page?: number; perPage?: number } ): QueryObserverResult { - const service = useGetEventFiltersService(); + const http = useHttp(); const { filter, page, perPage } = options; return useQuery( ['eventFilters', 'assigned', 'search', policyId, options], () => { - return service.getList({ + return getList({ + http, filter: parsePoliciesAndFilterToKql({ policies: [policyId, 'all'], kuery: parseQueryFilterToKQL(filter || '', SEARCHABLE_FIELDS), @@ -75,13 +71,14 @@ export function useSearchNotAssignedEventFilters( policyId: string, options: { filter?: string; perPage?: number; enabled?: boolean } ): QueryObserverResult { - const service = useGetEventFiltersService(); + const http = useHttp(); return useQuery( ['eventFilters', 'notAssigned', policyId, options], () => { const { filter, perPage } = options; - return service.getList({ + return getList({ + http, filter: parsePoliciesAndFilterToKql({ excludedPolicies: [policyId, 'all'], kuery: parseQueryFilterToKQL(filter || '', SEARCHABLE_FIELDS), @@ -106,7 +103,7 @@ export function useBulkUpdateEventFilters( onSettledCallback?: () => void; } = {} ) { - const service = useGetEventFiltersService(); + const http = useHttp(); const queryClient = useQueryClient(); const { @@ -125,7 +122,7 @@ export function useBulkUpdateEventFilters( return pMap( eventFilters, (eventFilter) => { - return service.updateOne(eventFilter); + return updateOne(http, eventFilter); }, { concurrency: 5, @@ -148,11 +145,11 @@ export function useGetAllEventFilters(): QueryObserverResult< FoundExceptionListItemSchema, ServerApiError > { - const service = useGetEventFiltersService(); + const http = useHttp(); return useQuery( ['eventFilters', 'all'], () => { - return service.getList(); + return getList({ http }); }, { refetchIntervalInBackground: false, From 067da143b6eba6802dbe00d26384e1c6482bd308 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 20 Jan 2022 11:42:13 -0500 Subject: [PATCH 05/43] Make duration line series use `local` for time zone value. (#123470) --- .../components/common/charts/duration_line_series_list.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx index adb76d4cbc8ac..bc744be163223 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx @@ -31,6 +31,7 @@ export const DurationLineSeriesList = ({ monitorType, lines }: Props) => ( yAccessors={[1]} yScaleType={ScaleType.Linear} fit={Fit.Linear} + timeZone="local" tickFormat={(d) => monitorType === 'browser' ? `${microToSec(d)} ${SEC_LABEL}` From 8b2c40bcb53736ede457dd9f944c1c4b196c524b Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 20 Jan 2022 09:03:56 -0800 Subject: [PATCH 06/43] Update more ML URLs in doc link service (#123432) --- src/core/public/doc_links/doc_links_service.ts | 18 +++++++++--------- .../ml/common/constants/messages.test.ts | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 4db97c1b9e256..b85572e650f26 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -350,19 +350,19 @@ export class DocLinksService { anomalyDetection: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-overview.html`, anomalyDetectionJobs: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html`, anomalyDetectionConfiguringCategories: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-categories.html`, - anomalyDetectionBucketSpan: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html`, - anomalyDetectionCardinality: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html`, - anomalyDetectionCreateJobs: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html`, - anomalyDetectionDetectors: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html`, - anomalyDetectionInfluencers: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html`, + anomalyDetectionBucketSpan: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-bucket-span`, + anomalyDetectionCardinality: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-cardinality`, + anomalyDetectionCreateJobs: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-create-job`, + anomalyDetectionDetectors: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-detectors`, + anomalyDetectionInfluencers: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-influencers`, anomalyDetectionJobResource: `${ELASTICSEARCH_DOCS}ml-put-job.html#ml-put-job-path-parms`, anomalyDetectionJobResourceAnalysisConfig: `${ELASTICSEARCH_DOCS}ml-put-job.html#put-analysisconfig`, - anomalyDetectionJobTips: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html`, + anomalyDetectionJobTips: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-job-tips`, alertingRules: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-alerts.html`, - anomalyDetectionModelMemoryLimits: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html`, - calendars: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html`, + anomalyDetectionModelMemoryLimits: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-model-memory-limits`, + calendars: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-calendars`, classificationEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-classification.html#ml-dfanalytics-classification-evaluation`, - customRules: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html`, + customRules: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-rules`, customUrls: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-url.html`, dataFrameAnalytics: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics.html`, featureImportance: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-feature-importance.html`, diff --git a/x-pack/plugins/ml/common/constants/messages.test.ts b/x-pack/plugins/ml/common/constants/messages.test.ts index 13ef103ae0ff1..e77de53b5cb70 100644 --- a/x-pack/plugins/ml/common/constants/messages.test.ts +++ b/x-pack/plugins/ml/common/constants/messages.test.ts @@ -32,7 +32,7 @@ describe('Constants: Messages parseMessages()', () => { id: 'detectors_function_not_empty', status: 'success', text: 'Presence of detector functions validated in all detectors.', - url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html', + url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-run-jobs.html#ml-ad-detectors', }, { bucketSpan: '15m', @@ -40,7 +40,7 @@ describe('Constants: Messages parseMessages()', () => { id: 'success_bucket_span', status: 'success', text: 'Format of "15m" is valid and passed validation checks.', - url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html', + url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-run-jobs.html#ml-ad-bucket-span', }, { heading: 'Time range', @@ -53,7 +53,7 @@ describe('Constants: Messages parseMessages()', () => { id: 'success_mml', status: 'success', text: 'Valid and within the estimated model memory limit.', - url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html', + url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-run-jobs.html#ml-ad-model-memory-limits', }, ]); }); @@ -71,7 +71,7 @@ describe('Constants: Messages parseMessages()', () => { id: 'detectors_function_not_empty', status: 'success', text: 'Presence of detector functions validated in all detectors.', - url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html', + url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-run-jobs.html#ml-ad-detectors', }, { bucketSpan: '15m', @@ -103,7 +103,7 @@ describe('Constants: Messages parseMessages()', () => { id: 'detectors_function_not_empty', status: 'success', text: 'Presence of detector functions validated in all detectors.', - url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html', + url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-run-jobs.html#ml-ad-detectors', }, { id: 'cardinality_model_plot_high', @@ -115,14 +115,14 @@ describe('Constants: Messages parseMessages()', () => { id: 'cardinality_partition_field', status: 'warning', text: 'Cardinality of partition_field "order_id" is above 1000 and might result in high memory usage.', - url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html', + url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-run-jobs.html#ml-ad-cardinality', }, { heading: 'Bucket span', id: 'bucket_span_high', status: 'info', text: 'Bucket span is 1 day or more. Be aware that days are considered as UTC days, not local days.', - url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html', + url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-run-jobs.html#ml-ad-bucket-span', }, { bucketSpanCompareFactor: 25, @@ -136,14 +136,14 @@ describe('Constants: Messages parseMessages()', () => { id: 'success_influencers', status: 'success', text: 'Influencer configuration passed the validation checks.', - url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html', + url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-run-jobs.html#ml-ad-influencers', }, { id: 'half_estimated_mml_greater_than_mml', mml: '1MB', status: 'warning', text: 'The specified model memory limit is less than half of the estimated model memory limit and will likely hit the hard limit.', - url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html', + url: 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-run-jobs.html#ml-ad-model-memory-limits', }, { id: 'missing_summary_count_field_name', From f4046b7f56f78a8e547d2e80cdf09ecf7c6e455b Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Thu, 20 Jan 2022 18:06:09 +0100 Subject: [PATCH 07/43] [Reporting] Add deprecation flag for v1 PDF and PNG export types (#123235) * Rename deprecated reporting jobs parameters interfaces * Add deprecation flag to the deprecated reporting jobs payloads --- .../common/types/export_types/png.ts | 5 +++- .../types/export_types/printable_pdf.ts | 7 +++-- .../plugins/reporting/common/types/index.ts | 4 +-- .../export_types/png/create_job/index.ts | 20 +++++++------ .../server/export_types/png/index.ts | 4 +-- .../server/export_types/png/types.ts | 5 +++- .../printable_pdf/create_job/index.ts | 28 ++++++++++--------- .../export_types/printable_pdf/index.ts | 4 +-- .../export_types/printable_pdf/types.ts | 2 +- .../server/routes/lib/request_handler.test.ts | 7 +++-- .../services/scenarios.ts | 8 +++--- 11 files changed, 54 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/reporting/common/types/export_types/png.ts b/x-pack/plugins/reporting/common/types/export_types/png.ts index 5afde424127a1..635de9823c8b3 100644 --- a/x-pack/plugins/reporting/common/types/export_types/png.ts +++ b/x-pack/plugins/reporting/common/types/export_types/png.ts @@ -15,7 +15,10 @@ interface BaseParamsPNG { } // Job params: structure of incoming user request data -export type JobParamsPNG = BaseParamsPNG & BaseParams; +/** + * @deprecated + */ +export type JobParamsPNGDeprecated = BaseParamsPNG & BaseParams; // Job payload: structure of stored job data provided by create_job export type TaskPayloadPNG = BaseParamsPNG & BasePayload; diff --git a/x-pack/plugins/reporting/common/types/export_types/printable_pdf.ts b/x-pack/plugins/reporting/common/types/export_types/printable_pdf.ts index 55a686cc829be..d5b8ec21cc13b 100644 --- a/x-pack/plugins/reporting/common/types/export_types/printable_pdf.ts +++ b/x-pack/plugins/reporting/common/types/export_types/printable_pdf.ts @@ -15,9 +15,12 @@ interface BaseParamsPDF { } // Job params: structure of incoming user request data, after being parsed from RISON -export type JobParamsPDF = BaseParamsPDF & BaseParams; +/** + * @deprecated + */ +export type JobParamsPDFDeprecated = BaseParamsPDF & BaseParams; -export type JobAppParamsPDF = Omit; +export type JobAppParamsPDF = Omit; // Job payload: structure of stored job data provided by create_job export interface TaskPayloadPDF extends BasePayload { diff --git a/x-pack/plugins/reporting/common/types/index.ts b/x-pack/plugins/reporting/common/types/index.ts index 4056b40a7f454..4ccd11f8522e6 100644 --- a/x-pack/plugins/reporting/common/types/index.ts +++ b/x-pack/plugins/reporting/common/types/index.ts @@ -7,9 +7,9 @@ import type { BaseParams, BaseParamsV2, BasePayload, BasePayloadV2, JobId } from './base'; -export type { JobParamsPNG } from './export_types/png'; +export type { JobParamsPNGDeprecated } from './export_types/png'; export type { JobParamsPNGV2 } from './export_types/png_v2'; -export type { JobAppParamsPDF, JobParamsPDF } from './export_types/printable_pdf'; +export type { JobAppParamsPDF, JobParamsPDFDeprecated } from './export_types/printable_pdf'; export type { JobAppParamsPDFV2, JobParamsPDFV2 } from './export_types/printable_pdf_v2'; export type { DownloadReportFn, diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index f18c0fe2778bd..70822c29119d5 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -7,16 +7,18 @@ import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; -import { JobParamsPNG, TaskPayloadPNG } from '../types'; +import { JobParamsPNGDeprecated, TaskPayloadPNG } from '../types'; -export const createJobFnFactory: CreateJobFnFactory> = - function createJobFactoryFn() { - return async function createJob(jobParams) { - validateUrls([jobParams.relativeUrl]); +export const createJobFnFactory: CreateJobFnFactory< + CreateJobFn +> = function createJobFactoryFn() { + return async function createJob(jobParams) { + validateUrls([jobParams.relativeUrl]); - return { - ...jobParams, - forceNow: new Date().toISOString(), - }; + return { + ...jobParams, + isDeprecated: true, + forceNow: new Date().toISOString(), }; }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/png/index.ts b/x-pack/plugins/reporting/server/export_types/png/index.ts index 7d3e65540fe3a..f4c3155c7b8a5 100644 --- a/x-pack/plugins/reporting/server/export_types/png/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/index.ts @@ -17,10 +17,10 @@ import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; import { createJobFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; import { metadata } from './metadata'; -import { JobParamsPNG, TaskPayloadPNG } from './types'; +import { JobParamsPNGDeprecated, TaskPayloadPNG } from './types'; export const getExportType = (): ExportTypeDefinition< - CreateJobFn, + CreateJobFn, RunTaskFn > => ({ ...metadata, diff --git a/x-pack/plugins/reporting/server/export_types/png/types.ts b/x-pack/plugins/reporting/server/export_types/png/types.ts index 8b8768467155e..3940f24b762a8 100644 --- a/x-pack/plugins/reporting/server/export_types/png/types.ts +++ b/x-pack/plugins/reporting/server/export_types/png/types.ts @@ -5,4 +5,7 @@ * 2.0. */ -export type { JobParamsPNG, TaskPayloadPNG } from '../../../common/types/export_types/png'; +export type { + JobParamsPNGDeprecated, + TaskPayloadPNG, +} from '../../../common/types/export_types/png'; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index 00702341f0dc9..eeaf0f82f1698 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -7,20 +7,22 @@ import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; -import { JobParamsPDF, TaskPayloadPDF } from '../types'; +import { JobParamsPDFDeprecated, TaskPayloadPDF } from '../types'; -export const createJobFnFactory: CreateJobFnFactory> = - function createJobFactoryFn() { - return async function createJobFn( - { relativeUrls, ...jobParams }: JobParamsPDF // relativeUrls does not belong in the payload of PDFV1 - ) { - validateUrls(relativeUrls); +export const createJobFnFactory: CreateJobFnFactory< + CreateJobFn +> = function createJobFactoryFn() { + return async function createJobFn( + { relativeUrls, ...jobParams }: JobParamsPDFDeprecated // relativeUrls does not belong in the payload of PDFV1 + ) { + validateUrls(relativeUrls); - // return the payload - return { - ...jobParams, - forceNow: new Date().toISOString(), - objects: relativeUrls.map((u) => ({ relativeUrl: u })), - }; + // return the payload + return { + ...jobParams, + isDeprecated: true, + forceNow: new Date().toISOString(), + objects: relativeUrls.map((u) => ({ relativeUrl: u })), }; }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts index d429c0fd6d001..4a4b63bae930b 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts @@ -17,10 +17,10 @@ import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; import { createJobFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; import { metadata } from './metadata'; -import { JobParamsPDF, TaskPayloadPDF } from './types'; +import { JobParamsPDFDeprecated, TaskPayloadPDF } from './types'; export const getExportType = (): ExportTypeDefinition< - CreateJobFn, + CreateJobFn, RunTaskFn > => ({ ...metadata, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.ts index f57d6a709fedb..ebf258f73a41f 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.ts @@ -6,6 +6,6 @@ */ export type { - JobParamsPDF, + JobParamsPDFDeprecated, TaskPayloadPDF, } from '../../../common/types/export_types/printable_pdf'; diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts index 13408db881317..0028073290f20 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts @@ -8,7 +8,7 @@ import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { ReportingCore } from '../..'; -import { JobParamsPDF, TaskPayloadPDF } from '../../export_types/printable_pdf/types'; +import { JobParamsPDFDeprecated, TaskPayloadPDF } from '../../export_types/printable_pdf/types'; import { Report, ReportingStore } from '../../lib/store'; import { ReportApiJSON } from '../../lib/store/report'; import { @@ -52,7 +52,7 @@ describe('Handle request to generate', () => { let mockResponseFactory: ReturnType; let requestHandler: RequestHandler; - const mockJobParams: JobParamsPDF = { + const mockJobParams: JobParamsPDFDeprecated = { browserTimezone: 'UTC', objectType: 'cool_object_type', title: 'cool_title', @@ -110,7 +110,7 @@ describe('Handle request to generate', () => { "kibana_name": undefined, "max_attempts": undefined, "meta": Object { - "isDeprecated": undefined, + "isDeprecated": true, "layout": "preserve_layout", "objectType": "cool_object_type", }, @@ -127,6 +127,7 @@ describe('Handle request to generate', () => { Object { "browserTimezone": "UTC", "headers": "hello mock cypher text", + "isDeprecated": true, "layout": Object { "id": "preserve_layout", }, diff --git a/x-pack/test/reporting_api_integration/services/scenarios.ts b/x-pack/test/reporting_api_integration/services/scenarios.ts index 6af60018d01da..6535694db042c 100644 --- a/x-pack/test/reporting_api_integration/services/scenarios.ts +++ b/x-pack/test/reporting_api_integration/services/scenarios.ts @@ -12,8 +12,8 @@ import { } from '../../../plugins/reporting/common/constants'; import { JobParamsCSV } from '../../../plugins/reporting/server/export_types/csv_searchsource/types'; import { JobParamsDownloadCSV } from '../../../plugins/reporting/server/export_types/csv_searchsource_immediate/types'; -import { JobParamsPNG } from '../../../plugins/reporting/server/export_types/png/types'; -import { JobParamsPDF } from '../../../plugins/reporting/server/export_types/printable_pdf/types'; +import { JobParamsPNGDeprecated } from '../../../plugins/reporting/server/export_types/png/types'; +import { JobParamsPDFDeprecated } from '../../../plugins/reporting/server/export_types/printable_pdf/types'; import { FtrProviderContext } from '../ftr_provider_context'; function removeWhitespace(str: string) { @@ -141,7 +141,7 @@ export function createScenarios({ getService }: Pick { + const generatePdf = async (username: string, password: string, job: JobParamsPDFDeprecated) => { const jobParams = rison.encode(job as object as RisonValue); return await supertestWithoutAuth .post(`/api/reporting/generate/printablePdf`) @@ -149,7 +149,7 @@ export function createScenarios({ getService }: Pick { + const generatePng = async (username: string, password: string, job: JobParamsPNGDeprecated) => { const jobParams = rison.encode(job as object as RisonValue); return await supertestWithoutAuth .post(`/api/reporting/generate/png`) From a3181a53387e838a76cd60237f71c235122275ed Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 20 Jan 2022 12:16:10 -0500 Subject: [PATCH 08/43] [Security Solution][Lists] Add API level validation for Trusted Application via Lists Plugin extension points (#122454) ## Lists Plugin changes: - Modified ExceptionListClient to accept an optional KibanaRequest when instantiating a new instance of the class - Changes the extension points callback argument structure to an object having context and data. Context provides to the callbacks the HTTP request so that additional validation can be performed (ex. Authz to certain features) - ExtensionPointStorageClient#pipeRun() will now throw if an extension point callback also throws an error (instead of logging it and continuing on with callback execution) - ErrorWithStatusCode was export'ed out of the server (as ListsErrorWithStatusCode) and available for use by dependent plugins ## Security Solution Plugin (endpoint) changes: - Added new getEndpointAuthz(request) and getExceptionListsClient() methods to EndpointAppContextService - Added new server lists integration modules. Registers extension points with the Lists plugin for create and update of exception items. Currently validates only Trusted Apps - Added exception item artifact validators: - a BaseValidator with several generic and reusable methods that can be applied to any artifact - a TrustedAppValidator to specifically validate Trusted Applications - Refactor: - moved EndpointFleetServices to its own folder and also renamed it to include the word Factory (will help in the future if we create server-side service clients for working with Endpoint Policies) - Created common Artifact utilities and const's for working with ExceptionListItemSchema items --- x-pack/plugins/lists/server/index.ts | 5 +- x-pack/plugins/lists/server/mocks.ts | 6 +- x-pack/plugins/lists/server/plugin.ts | 1 + .../exception_list_client.test.ts | 9 +- .../exception_lists/exception_list_client.ts | 18 +- .../exception_list_client_types.ts | 4 +- .../extension_point_storage.mock.ts | 15 +- .../extension_point_storage_client.test.ts | 103 ++++---- .../extension_point_storage_client.ts | 30 +-- .../server/services/extension_points/types.ts | 55 +++-- x-pack/plugins/lists/server/types.ts | 2 +- .../exceptions_list_item_generator.ts | 152 +++++++++++- .../endpoint/service/artifacts/constants.ts | 10 + .../endpoint/service/artifacts/index.ts | 10 + .../endpoint/service/artifacts/utils.ts | 32 +++ .../endpoint/service/trusted_apps/mapping.ts | 9 +- .../service/trusted_apps/validations.ts | 6 +- .../effected_policy_select/utils.ts | 9 +- .../pages/mocks/trusted_apps_http_mocks.ts | 6 +- .../pages/trusted_apps/service/mappers.ts | 10 +- .../view/trusted_apps_page.test.tsx | 1 - .../endpoint/endpoint_app_context_services.ts | 53 ++-- .../server/endpoint/mocks.ts | 32 ++- .../endpoint/routes/metadata/handlers.ts | 2 +- .../endpoint/routes/policy/handlers.test.ts | 24 +- .../endpoint_fleet_services_factory.ts} | 14 +- .../server/endpoint/services/fleet/index.ts | 8 + .../metadata/endpoint_metadata_service.ts | 2 +- .../endpoint/services/metadata/mocks.ts | 17 +- .../handlers/exceptions_pre_create_handler.ts | 26 ++ .../handlers/exceptions_pre_update_handler.ts | 43 ++++ .../register_endpoint_extension_points.ts | 28 +++ .../lists_integration/endpoint/types.ts | 19 ++ .../validators/base_validator.test.ts | 187 ++++++++++++++ .../endpoint/validators/base_validator.ts | 164 +++++++++++++ .../endpoint/validators/errors.ts | 14 ++ .../endpoint/validators/index.ts | 8 + .../endpoint/validators/mocks.ts | 60 +++++ .../validators/trusted_app_validator.ts | 224 +++++++++++++++++ .../server/lists_integration/index.ts | 8 + .../server/lists_integration/jest.config.js | 26 ++ .../security_solution/server/plugin.ts | 29 ++- .../services/endpoint_artifacts.ts | 89 +++++++ .../services/index.ts | 2 + .../apis/endpoint_artifacts.ts | 229 ++++++++++++++++++ .../apis/index.ts | 1 + .../services/index.ts | 4 + 47 files changed, 1634 insertions(+), 172 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/artifacts/constants.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/artifacts/index.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/artifacts/utils.ts rename x-pack/plugins/security_solution/server/endpoint/services/{endpoint_fleet_services.ts => fleet/endpoint_fleet_services_factory.ts} (81%) create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/fleet/index.ts create mode 100644 x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_create_handler.ts create mode 100644 x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts create mode 100644 x-pack/plugins/security_solution/server/lists_integration/endpoint/register_endpoint_extension_points.ts create mode 100644 x-pack/plugins/security_solution/server/lists_integration/endpoint/types.ts create mode 100644 x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts create mode 100644 x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts create mode 100644 x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/errors.ts create mode 100644 x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/index.ts create mode 100644 x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/mocks.ts create mode 100644 x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts create mode 100644 x-pack/plugins/security_solution/server/lists_integration/index.ts create mode 100644 x-pack/plugins/security_solution/server/lists_integration/jest.config.js create mode 100644 x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts diff --git a/x-pack/plugins/lists/server/index.ts b/x-pack/plugins/lists/server/index.ts index cbdd4722dde9f..dd7fbf4548f75 100644 --- a/x-pack/plugins/lists/server/index.ts +++ b/x-pack/plugins/lists/server/index.ts @@ -23,12 +23,15 @@ export type { ListsServerExtensionRegistrar, ExtensionPoint, ExceptionsListPreCreateItemServerExtension, - ExceptionListPreUpdateItemServerExtension, + ExceptionsListPreUpdateItemServerExtension, } from './types'; export type { ExportExceptionListAndItemsReturn } from './services/exception_lists/export_exception_list_and_items'; export const config: PluginConfigDescriptor = { schema: ConfigSchema, }; + +export { ErrorWithStatusCode as ListsErrorWithStatusCode } from './error_with_status_code'; + export const plugin = (initializerContext: PluginInitializerContext): ListPlugin => new ListPlugin(initializerContext); diff --git a/x-pack/plugins/lists/server/mocks.ts b/x-pack/plugins/lists/server/mocks.ts index 9cc130fc7d0d3..642fff817949e 100644 --- a/x-pack/plugins/lists/server/mocks.ts +++ b/x-pack/plugins/lists/server/mocks.ts @@ -7,7 +7,10 @@ import { ListPluginSetup } from './types'; import { getListClientMock } from './services/lists/list_client.mock'; -import { getExceptionListClientMock } from './services/exception_lists/exception_list_client.mock'; +import { + getCreateExceptionListItemOptionsMock, + getExceptionListClientMock, +} from './services/exception_lists/exception_list_client.mock'; const createSetupMock = (): jest.Mocked => { const mock: jest.Mocked = { @@ -20,6 +23,7 @@ const createSetupMock = (): jest.Mocked => { export const listMock = { createSetup: createSetupMock, + getCreateExceptionListItemOptionsMock, getExceptionListClient: getExceptionListClientMock, getListClient: getListClientMock, }; diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts index 5e59cbe2ba719..c824c6889c362 100644 --- a/x-pack/plugins/lists/server/plugin.ts +++ b/x-pack/plugins/lists/server/plugin.ts @@ -114,6 +114,7 @@ export class ListPlugin implements Plugin new ExceptionListClient({ + request, savedObjectsClient, serverExtensionsClient: this.extensionPoints.getClient(), user, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts index ed072b0ed7a92..c94b956c92bf7 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts @@ -11,7 +11,7 @@ import { ExtensionPointStorageContextMock, createExtensionPointStorageMock, } from '../extension_points/extension_point_storage.mock'; -import type { ExtensionPointCallbackArgument } from '../extension_points'; +import type { ExtensionPointCallbackDataArgument } from '../extension_points'; import { getCreateExceptionListItemOptionsMock, @@ -112,10 +112,12 @@ describe('exception_list_client', () => { it('should validate extension point callback returned data and throw if not valid', async () => { const extensionPointCallback = getExtensionPointCallback(); extensionPointCallback.mockImplementation(async (args) => { - const { entries, ...rest } = args as ExtensionPointCallbackArgument; + const { + data: { entries, ...rest }, + } = args as { data: ExtensionPointCallbackDataArgument }; expect(entries).toBeTruthy(); // Test entries to exist since we exclude it. - return rest as ExtensionPointCallbackArgument; + return rest as ExtensionPointCallbackDataArgument; }); const methodResponsePromise = callExceptionListClientMethod(); @@ -126,6 +128,7 @@ describe('exception_list_client', () => { reason: ['Invalid value "undefined" supplied to "entries"'], }) ); + expect(extensionPointStorageContext.logger.error).toHaveBeenCalled(); }); it('should use data returned from extension point callbacks when saving', async () => { diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index 061a4d305821b..61dd47867b3ff 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; import { ExceptionListItemSchema, ExceptionListSchema, @@ -18,7 +18,10 @@ import { } from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; -import type { ExtensionPointStorageClientInterface } from '../extension_points'; +import type { + ExtensionPointStorageClientInterface, + ServerExtensionCallbackContext, +} from '../extension_points'; import { ConstructorOptions, @@ -81,17 +84,26 @@ export class ExceptionListClient { private readonly savedObjectsClient: SavedObjectsClientContract; private readonly serverExtensionsClient: ExtensionPointStorageClientInterface; private readonly enableServerExtensionPoints: boolean; + private readonly request?: KibanaRequest; constructor({ user, savedObjectsClient, serverExtensionsClient, enableServerExtensionPoints = true, + request, }: ConstructorOptions) { this.user = user; this.savedObjectsClient = savedObjectsClient; this.serverExtensionsClient = serverExtensionsClient; this.enableServerExtensionPoints = enableServerExtensionPoints; + this.request = request; + } + + private getServerExtensionCallbackContext(): ServerExtensionCallbackContext { + return { + request: this.request, + }; } /** @@ -404,6 +416,7 @@ export class ExceptionListClient { itemData = await this.serverExtensionsClient.pipeRun( 'exceptionsListPreCreateItem', itemData, + this.getServerExtensionCallbackContext(), (data) => { return validateData( createExceptionListItemSchema, @@ -470,6 +483,7 @@ export class ExceptionListClient { updatedItem = await this.serverExtensionsClient.pipeRun( 'exceptionsListPreUpdateItem', updatedItem, + this.getServerExtensionCallbackContext(), (data) => { return validateData( updateExceptionListItemSchema, 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 93a45358daafa..e4fdd5ef2b5ce 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 @@ -7,7 +7,7 @@ import { Readable } from 'stream'; -import { SavedObjectsClientContract } from 'kibana/server'; +import type { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; import type { CreateCommentsArray, Description, @@ -58,6 +58,8 @@ export interface ConstructorOptions { serverExtensionsClient: ExtensionPointStorageClientInterface; /** Set to `false` if wanting to disable executing registered server extension points. Default is true. */ enableServerExtensionPoints?: boolean; + /** Should be provided when creating an instance from an HTTP request handler */ + request?: KibanaRequest; } export interface GetExceptionListOptions { diff --git a/x-pack/plugins/lists/server/services/extension_points/extension_point_storage.mock.ts b/x-pack/plugins/lists/server/services/extension_points/extension_point_storage.mock.ts index 2610e9808dac4..fb453e00492c7 100644 --- a/x-pack/plugins/lists/server/services/extension_points/extension_point_storage.mock.ts +++ b/x-pack/plugins/lists/server/services/extension_points/extension_point_storage.mock.ts @@ -7,11 +7,14 @@ import { MockedLogger, loggerMock } from '@kbn/logging/mocks'; +import { httpServerMock } from '../../../../../../src/core/server/mocks'; + import { ExtensionPointStorage } from './extension_point_storage'; import { - ExceptionListPreUpdateItemServerExtension, ExceptionsListPreCreateItemServerExtension, + ExceptionsListPreUpdateItemServerExtension, ExtensionPointStorageInterface, + ServerExtensionCallbackContext, } from './types'; export interface ExtensionPointStorageContextMock { @@ -21,7 +24,8 @@ export interface ExtensionPointStorageContextMock { /** An Exception List Item pre-create extension point added to the storage. Appends `-1` to the data's `name` attribute */ exceptionPreCreate: jest.Mocked; /** An Exception List Item pre-update extension point added to the storage. Appends `-2` to the data's `name` attribute */ - exceptionPreUpdate: jest.Mocked; + exceptionPreUpdate: jest.Mocked; + callbackContext: jest.Mocked; } export const createExtensionPointStorageMock = ( @@ -30,7 +34,7 @@ export const createExtensionPointStorageMock = ( const extensionPointStorage = new ExtensionPointStorage(logger); const exceptionPreCreate: ExtensionPointStorageContextMock['exceptionPreCreate'] = { - callback: jest.fn(async (data) => { + callback: jest.fn(async ({ data }) => { return { ...data, name: `${data.name}-1`, @@ -40,7 +44,7 @@ export const createExtensionPointStorageMock = ( }; const exceptionPreUpdate: ExtensionPointStorageContextMock['exceptionPreUpdate'] = { - callback: jest.fn(async (data) => { + callback: jest.fn(async ({ data }) => { return { ...data, name: `${data.name}-1`, @@ -53,6 +57,9 @@ export const createExtensionPointStorageMock = ( extensionPointStorage.add(exceptionPreUpdate); return { + callbackContext: { + request: httpServerMock.createKibanaRequest(), + }, exceptionPreCreate, exceptionPreUpdate, extensionPointStorage, diff --git a/x-pack/plugins/lists/server/services/extension_points/extension_point_storage_client.test.ts b/x-pack/plugins/lists/server/services/extension_points/extension_point_storage_client.test.ts index b7a687064e2e2..25d4a4902a0b7 100644 --- a/x-pack/plugins/lists/server/services/extension_points/extension_point_storage_client.test.ts +++ b/x-pack/plugins/lists/server/services/extension_points/extension_point_storage_client.test.ts @@ -13,11 +13,12 @@ import { DataValidationError } from '../exception_lists/utils/errors'; import { ExtensionPointError } from './errors'; import { - ExceptionListPreUpdateItemServerExtension, ExceptionsListPreCreateItemServerExtension, + ExceptionsListPreUpdateItemServerExtension, ExtensionPoint, ExtensionPointStorageClientInterface, ExtensionPointStorageInterface, + ServerExtensionCallbackContext, } from './types'; import { createExtensionPointStorageMock } from './extension_point_storage.mock'; @@ -25,6 +26,7 @@ describe('When using the ExtensionPointStorageClient', () => { let storageClient: ExtensionPointStorageClientInterface; let logger: ReturnType; let extensionPointStorage: ExtensionPointStorageInterface; + let callbackContext: ServerExtensionCallbackContext; let preCreateExtensionPointMock1: jest.Mocked; let extensionPointsMocks: Array>; let callbackRunLog: string; @@ -34,10 +36,10 @@ describe('When using the ExtensionPointStorageClient', () => { }; beforeEach(() => { - const storageContext = createExtensionPointStorageMock(); + const extensionPointStorageMock = createExtensionPointStorageMock(); callbackRunLog = ''; - ({ logger, extensionPointStorage } = storageContext); + ({ logger, extensionPointStorage, callbackContext } = extensionPointStorageMock); extensionPointStorage.clear(); // Generic callback function that also logs to the `callbackRunLog` its id, so we know the order in which they ran. @@ -48,12 +50,12 @@ describe('When using the ExtensionPointStorageClient', () => { A extends Parameters[0] = Parameters[0] >( id: number, - arg: A - ): Promise => { + { data }: A + ): Promise => { callbackRunLog += id; return { - ...arg, - name: `${arg.name}-${id}`, + ...data, + name: `${data.name}-${id}`, }; }; preCreateExtensionPointMock1 = { @@ -72,7 +74,7 @@ describe('When using the ExtensionPointStorageClient', () => { }, { callback: jest.fn( - callbackFn.bind(window, 3) as ExceptionListPreUpdateItemServerExtension['callback'] + callbackFn.bind(window, 3) as ExceptionsListPreUpdateItemServerExtension['callback'] ), type: 'exceptionsListPreUpdateItem', }, @@ -115,38 +117,70 @@ describe('When using the ExtensionPointStorageClient', () => { it('should run extension point callbacks serially', async () => { await storageClient.pipeRun( 'exceptionsListPreCreateItem', - createExceptionListItemOptionsMock + createExceptionListItemOptionsMock, + callbackContext ); expect(callbackRunLog).toEqual('1245'); }); - it('should pass the return value of one extensionPoint to the next', async () => { + it('should provide `context` to every callback', async () => { await storageClient.pipeRun( 'exceptionsListPreCreateItem', - createExceptionListItemOptionsMock + createExceptionListItemOptionsMock, + callbackContext ); + for (const extensionPointsMock of extensionPointsMocks) { + if (extensionPointsMock.type === 'exceptionsListPreCreateItem') { + expect(extensionPointsMock.callback).toHaveBeenCalledWith( + expect.objectContaining({ + context: { + request: expect.any(Object), + }, + }) + ); + } + } + }); - expect(extensionPointsMocks[0].callback).toHaveBeenCalledWith( - createExceptionListItemOptionsMock + it('should pass the return value of one extensionPoint to the next', async () => { + await storageClient.pipeRun( + 'exceptionsListPreCreateItem', + createExceptionListItemOptionsMock, + callbackContext ); + + expect(extensionPointsMocks[0].callback).toHaveBeenCalledWith({ + context: callbackContext, + data: createExceptionListItemOptionsMock, + }); expect(extensionPointsMocks[1].callback).toHaveBeenCalledWith({ - ...createExceptionListItemOptionsMock, - name: `${createExceptionListItemOptionsMock.name}-1`, + context: callbackContext, + data: { + ...createExceptionListItemOptionsMock, + name: `${createExceptionListItemOptionsMock.name}-1`, + }, }); expect(extensionPointsMocks[3].callback).toHaveBeenCalledWith({ - ...createExceptionListItemOptionsMock, - name: `${createExceptionListItemOptionsMock.name}-1-2`, + context: callbackContext, + data: { + ...createExceptionListItemOptionsMock, + name: `${createExceptionListItemOptionsMock.name}-1-2`, + }, }); expect(extensionPointsMocks[4].callback).toHaveBeenCalledWith({ - ...createExceptionListItemOptionsMock, - name: `${createExceptionListItemOptionsMock.name}-1-2-4`, + context: callbackContext, + data: { + ...createExceptionListItemOptionsMock, + name: `${createExceptionListItemOptionsMock.name}-1-2-4`, + }, }); }); it('should return a data structure similar to the one provided initially', async () => { const result = await storageClient.pipeRun( 'exceptionsListPreCreateItem', - createExceptionListItemOptionsMock + createExceptionListItemOptionsMock, + callbackContext ); expect(result).toEqual({ @@ -155,36 +189,20 @@ describe('When using the ExtensionPointStorageClient', () => { }); }); - it("should log an error if extension point callback Throw's", async () => { - const extensionError = new Error('foo'); - preCreateExtensionPointMock1.callback.mockImplementation(async () => { - throw extensionError; - }); - - await storageClient.pipeRun( - 'exceptionsListPreCreateItem', - createExceptionListItemOptionsMock - ); - - expect(logger.error).toHaveBeenCalledWith(expect.any(ExtensionPointError)); - expect(logger.error.mock.calls[0][0]).toMatchObject({ meta: extensionError }); - }); - - it('should continue to other extension points after encountering one that `throw`s', async () => { + it('should stop execution of other extension points after encountering one that `throw`s', async () => { const extensionError = new Error('foo'); preCreateExtensionPointMock1.callback.mockImplementation(async () => { throw extensionError; }); - const result = await storageClient.pipeRun( + const resultPromise = storageClient.pipeRun( 'exceptionsListPreCreateItem', - createExceptionListItemOptionsMock + createExceptionListItemOptionsMock, + callbackContext ); - expect(result).toEqual({ - ...createExceptionListItemOptionsMock, - name: `${createExceptionListItemOptionsMock.name}-2-4-5`, - }); + await expect(resultPromise).rejects.toBe(extensionError); + expect(extensionPointsMocks[1].callback).not.toHaveBeenCalled(); }); it('should log an error and Throw if external callback returned invalid data', async () => { @@ -194,6 +212,7 @@ describe('When using the ExtensionPointStorageClient', () => { storageClient.pipeRun( 'exceptionsListPreCreateItem', createExceptionListItemOptionsMock, + callbackContext, () => { return validationError; } diff --git a/x-pack/plugins/lists/server/services/extension_points/extension_point_storage_client.ts b/x-pack/plugins/lists/server/services/extension_points/extension_point_storage_client.ts index e430972157b75..453d5deccc8c8 100644 --- a/x-pack/plugins/lists/server/services/extension_points/extension_point_storage_client.ts +++ b/x-pack/plugins/lists/server/services/extension_points/extension_point_storage_client.ts @@ -9,10 +9,11 @@ import { Logger } from 'kibana/server'; import type { ExtensionPoint, - ExtensionPointCallbackArgument, + ExtensionPointCallbackDataArgument, ExtensionPointStorageClientInterface, ExtensionPointStorageInterface, NarrowExtensionPointToType, + ServerExtensionCallbackContext, } from './types'; import { ExtensionPointError } from './errors'; @@ -38,6 +39,7 @@ export class ExtensionPointStorageClient implements ExtensionPointStorageClientI * * @param extensionType * @param initialCallbackInput The initial argument given to the first extension point callback + * @param callbackContext * @param callbackResponseValidator A function to validate the returned data from an extension point callback */ async pipeRun< @@ -46,9 +48,10 @@ export class ExtensionPointStorageClient implements ExtensionPointStorageClientI P extends Parameters = Parameters >( extensionType: T, - initialCallbackInput: P[0], - callbackResponseValidator?: (data: P[0]) => Error | undefined - ): Promise { + initialCallbackInput: P[0]['data'], + callbackContext: ServerExtensionCallbackContext, + callbackResponseValidator?: (data: P[0]['data']) => Error | undefined + ): Promise { let inputArgument = initialCallbackInput; const externalExtensions = this.get(extensionType); @@ -60,26 +63,17 @@ export class ExtensionPointStorageClient implements ExtensionPointStorageClientI const extensionRegistrationSource = this.storage.getExtensionRegistrationSource(externalExtension); - try { - inputArgument = await externalExtension.callback( - inputArgument as ExtensionPointCallbackArgument - ); - } catch (error) { - // Log the error that the external callback threw and keep going with the running of others - this.logger?.error( - new ExtensionPointError( - `Extension point execution error for ${externalExtension.type}: ${extensionRegistrationSource}`, - error - ) - ); - } + inputArgument = await externalExtension.callback({ + context: callbackContext, + data: inputArgument as ExtensionPointCallbackDataArgument, + }); if (callbackResponseValidator) { // Before calling the next one, make sure the returned payload is valid const validationError = callbackResponseValidator(inputArgument); if (validationError) { - this.logger?.error( + this.logger.error( new ExtensionPointError( `Extension point for ${externalExtension.type} returned data that failed validation: ${extensionRegistrationSource}`, { diff --git a/x-pack/plugins/lists/server/services/extension_points/types.ts b/x-pack/plugins/lists/server/services/extension_points/types.ts index 7ae2b5971f825..3dcec1fae63e8 100644 --- a/x-pack/plugins/lists/server/services/extension_points/types.ts +++ b/x-pack/plugins/lists/server/services/extension_points/types.ts @@ -5,24 +5,51 @@ * 2.0. */ -import { PromiseType } from 'utility-types'; import { UnionToIntersection } from '@kbn/utility-types'; +import { KibanaRequest } from 'kibana/server'; import { CreateExceptionListItemOptions, UpdateExceptionListItemOptions, } from '../exception_lists/exception_list_client_types'; -export type ServerExtensionCallback = ( - args: A -) => Promise; +/** + * The `this` context provided to extension point's callback function + * NOTE: in order to access this context, callbacks **MUST** be defined using `function()` instead of arrow functions. + */ +export interface ServerExtensionCallbackContext { + /** + * The Lists plugin HTTP Request. May be undefined if the callback is executed from a area of code that + * is not triggered via one of the HTTP handlers + */ + request?: KibanaRequest; +} + +export type ServerExtensionCallback = (args: { + context: ServerExtensionCallbackContext; + data: A; +}) => Promise; interface ServerExtensionPointDefinition< T extends string, Args extends object | void = void, - Response = void + Response = Args > { type: T; + /** + * The callback that will be executed at the given extension point. The Function will be provided a context (`this)` + * that includes supplemental data associated with its type. In order to access that data, the callback **MUST** + * be defined using `function()` and NOT an arrow function. + * + * @example + * + * { + * type: 'some type', + * callback: function() { + * // this === context is available + * } + * } + */ callback: ServerExtensionCallback; } @@ -32,7 +59,6 @@ interface ServerExtensionPointDefinition< */ export type ExceptionsListPreCreateItemServerExtension = ServerExtensionPointDefinition< 'exceptionsListPreCreateItem', - CreateExceptionListItemOptions, CreateExceptionListItemOptions >; @@ -40,15 +66,14 @@ export type ExceptionsListPreCreateItemServerExtension = ServerExtensionPointDef * Extension point is triggered prior to updating the Exception List Item. Throw'ing will cause the * update operation to fail */ -export type ExceptionListPreUpdateItemServerExtension = ServerExtensionPointDefinition< +export type ExceptionsListPreUpdateItemServerExtension = ServerExtensionPointDefinition< 'exceptionsListPreUpdateItem', - UpdateExceptionListItemOptions, UpdateExceptionListItemOptions >; export type ExtensionPoint = | ExceptionsListPreCreateItemServerExtension - | ExceptionListPreUpdateItemServerExtension; + | ExceptionsListPreUpdateItemServerExtension; /** * A Map of extension point type and associated Set of callbacks @@ -57,6 +82,7 @@ export type ExtensionPoint = * Registration function for server-side extension points */ export type ListsServerExtensionRegistrar = (extension: ExtensionPoint) => void; + export type NarrowExtensionPointToType = { type: T; } & ExtensionPoint; @@ -65,8 +91,8 @@ export type NarrowExtensionPointToType = { * An intersection of all callback arguments for use internally when * casting (ex. in `ExtensionPointStorageClient#pipeRun()` */ -export type ExtensionPointCallbackArgument = UnionToIntersection< - PromiseType> +export type ExtensionPointCallbackDataArgument = UnionToIntersection< + Parameters[0]['data'] >; export interface ExtensionPointStorageClientInterface { @@ -80,9 +106,10 @@ export interface ExtensionPointStorageClientInterface { P extends Parameters = Parameters >( extensionType: T, - initialCallbackInput: P[0], - callbackResponseValidator?: (data: P[0]) => Error | undefined - ): Promise; + initialCallbackInput: P[0]['data'], + callbackContext: ServerExtensionCallbackContext, + callbackResponseValidator?: (data: P[0]['data']) => Error | undefined + ): Promise; } export interface ExtensionPointStorageInterface { diff --git a/x-pack/plugins/lists/server/types.ts b/x-pack/plugins/lists/server/types.ts index cdaa81f697117..4eece62b9b0a3 100644 --- a/x-pack/plugins/lists/server/types.ts +++ b/x-pack/plugins/lists/server/types.ts @@ -75,7 +75,7 @@ export type ContextProviderReturn = Promise; export type { ExtensionPoint, - ExceptionListPreUpdateItemServerExtension, + ExceptionsListPreUpdateItemServerExtension, ExceptionsListPreCreateItemServerExtension, ListsServerExtensionRegistrar, } from './services/extension_points'; diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts index d66f1e950c34f..3ff333232a677 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts @@ -5,10 +5,34 @@ * 2.0. */ -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import type { + ExceptionListItemSchema, + CreateExceptionListItemSchema, + UpdateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { BaseDataGenerator } from './base_data_generator'; -import { POLICY_REFERENCE_PREFIX } from '../service/trusted_apps/mapping'; import { ConditionEntryField } from '../types'; +import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../service/artifacts/constants'; + +/** Utility that removes null and undefined from a Type's property value */ +type NonNullableTypeProperties = { + [P in keyof T]-?: NonNullable; +}; + +/** + * Normalizes the create type to remove `undefined`/`null` from the returned type since the generator or sure to + * create a value for (almost) all properties + */ +type CreateExceptionListItemSchemaWithNonNullProps = NonNullableTypeProperties< + Omit +> & + Pick; + +type UpdateExceptionListItemSchemaWithNonNullProps = NonNullableTypeProperties< + Omit +> & + Pick; export class ExceptionsListItemGenerator extends BaseDataGenerator { generate(overrides: Partial = {}): ExceptionListItemSchema { @@ -38,11 +62,11 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator = {} + ): CreateExceptionListItemSchemaWithNonNullProps { + const { + /* eslint-disable @typescript-eslint/naming-convention */ + description, + entries, + list_id, + name, + type, + comments, + item_id, + meta, + namespace_type, + os_types, + tags, + /* eslint-enable @typescript-eslint/naming-convention */ + } = this.generate(); + + return { + description, + entries, + list_id, + name, + type, + comments, + item_id, + meta, + namespace_type, + os_types, + tags, + ...overrides, + }; + } + + generateTrustedApp(overrides: Partial = {}): ExceptionListItemSchema { + const trustedApp = this.generate(overrides); + + return { + ...trustedApp, + name: `Trusted app (${this.randomString(5)})`, + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + // Remove the hash field which the generator above currently still sets to a field that is not + // actually valid when used with the Exception List + entries: trustedApp.entries.filter((entry) => entry.field !== ConditionEntryField.HASH), + }; + } + + generateTrustedAppForCreate( + overrides: Partial = {} + ): CreateExceptionListItemSchemaWithNonNullProps { + const { + /* eslint-disable @typescript-eslint/naming-convention */ + description, + entries, + list_id, + name, + type, + comments, + item_id, + meta, + namespace_type, + os_types, + tags, + /* eslint-enable @typescript-eslint/naming-convention */ + } = this.generateTrustedApp(); + + return { + description, + entries, + list_id, + name, + type, + comments, + item_id, + meta, + namespace_type, + os_types, + tags, + ...overrides, + }; + } + + generateTrustedAppForUpdate( + overrides: Partial = {} + ): UpdateExceptionListItemSchemaWithNonNullProps { + const { + /* eslint-disable @typescript-eslint/naming-convention */ + description, + entries, + name, + type, + comments, + id, + item_id, + meta, + namespace_type, + os_types, + tags, + _version, + /* eslint-enable @typescript-eslint/naming-convention */ + } = this.generateTrustedApp(); + + return { + description, + entries, + name, + type, + comments, + id, + item_id, + meta, + namespace_type, + os_types, + tags, + _version: _version ?? 'some value', + ...overrides, + }; + } } diff --git a/x-pack/plugins/security_solution/common/endpoint/service/artifacts/constants.ts b/x-pack/plugins/security_solution/common/endpoint/service/artifacts/constants.ts new file mode 100644 index 0000000000000..eadc52941da05 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/artifacts/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 const BY_POLICY_ARTIFACT_TAG_PREFIX = 'policy:'; + +export const GLOBAL_ARTIFACT_TAG = `${BY_POLICY_ARTIFACT_TAG_PREFIX}all`; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/artifacts/index.ts b/x-pack/plugins/security_solution/common/endpoint/service/artifacts/index.ts new file mode 100644 index 0000000000000..6bdf0fb59a3a8 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/artifacts/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { isArtifactGlobal, isArtifactByPolicy, getPolicyIdsFromArtifact } from './utils'; + +export { BY_POLICY_ARTIFACT_TAG_PREFIX, GLOBAL_ARTIFACT_TAG } from './constants'; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/artifacts/utils.ts b/x-pack/plugins/security_solution/common/endpoint/service/artifacts/utils.ts new file mode 100644 index 0000000000000..4cc39e9fb8980 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/artifacts/utils.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { BY_POLICY_ARTIFACT_TAG_PREFIX, GLOBAL_ARTIFACT_TAG } from './constants'; + +const POLICY_ID_START_POSITION = BY_POLICY_ARTIFACT_TAG_PREFIX.length; + +export const isArtifactGlobal = (item: Pick): boolean => { + return (item.tags ?? []).find((tag) => tag === GLOBAL_ARTIFACT_TAG) !== undefined; +}; + +export const isArtifactByPolicy = (item: Pick): boolean => { + return !isArtifactGlobal(item); +}; + +export const getPolicyIdsFromArtifact = (item: Pick): string[] => { + const policyIds = []; + const tags = item.tags ?? []; + + for (const tag of tags) { + if (tag !== GLOBAL_ARTIFACT_TAG && tag.startsWith(BY_POLICY_ARTIFACT_TAG_PREFIX)) { + policyIds.push(tag.substring(POLICY_ID_START_POSITION)); + } + } + + return policyIds; +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/mapping.ts index ddc366f49ed95..cf36c6911ca91 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/mapping.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/mapping.ts @@ -6,8 +6,7 @@ */ import { EffectScope } from '../../types'; - -export const POLICY_REFERENCE_PREFIX = 'policy:'; +import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../artifacts/constants'; /** * Looks at an array of `tags` (attributed defined on the `ExceptionListItemSchema`) and returns back @@ -15,16 +14,16 @@ export const POLICY_REFERENCE_PREFIX = 'policy:'; * @param tags */ export const tagsToEffectScope = (tags: string[]): EffectScope => { - const policyReferenceTags = tags.filter((tag) => tag.startsWith(POLICY_REFERENCE_PREFIX)); + const policyReferenceTags = tags.filter((tag) => tag.startsWith(BY_POLICY_ARTIFACT_TAG_PREFIX)); - if (policyReferenceTags.some((tag) => tag === `${POLICY_REFERENCE_PREFIX}all`)) { + if (policyReferenceTags.some((tag) => tag === `${BY_POLICY_ARTIFACT_TAG_PREFIX}all`)) { return { type: 'global', }; } else { return { type: 'policy', - policies: policyReferenceTags.map((tag) => tag.substr(POLICY_REFERENCE_PREFIX.length)), + policies: policyReferenceTags.map((tag) => tag.substr(BY_POLICY_ARTIFACT_TAG_PREFIX.length)), }; } }; 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 3fee05e2f0061..0e6f2a5a7df41 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 @@ -26,7 +26,11 @@ export const getDuplicateFields = (entries: ConditionEntry[]) => { const groupedFields = new Map(); entries.forEach((entry) => { - groupedFields.set(entry.field, [...(groupedFields.get(entry.field) || []), entry]); + // With the move to the Exception Lists api, the server side now validates individual + // `process.hash.[type]`'s, so we need to account for that here + const field = entry.field.startsWith('process.hash') ? ConditionEntryField.HASH : entry.field; + + groupedFields.set(field, [...(groupedFields.get(field) || []), entry]); }); return [...groupedFields.entries()] diff --git a/x-pack/plugins/security_solution/public/management/components/effected_policy_select/utils.ts b/x-pack/plugins/security_solution/public/management/components/effected_policy_select/utils.ts index b3c1dd6648f83..3f90df40391bc 100644 --- a/x-pack/plugins/security_solution/public/management/components/effected_policy_select/utils.ts +++ b/x-pack/plugins/security_solution/public/management/components/effected_policy_select/utils.ts @@ -7,8 +7,7 @@ import { PolicyData } from '../../../../common/endpoint/types'; import { EffectedPolicySelection } from './effected_policy_select'; - -export const GLOBAL_POLICY_TAG = 'policy:all'; +import { GLOBAL_ARTIFACT_TAG } from '../../../../common/endpoint/service/artifacts/constants'; /** * Given a list of artifact tags, returns the tags that are not policy tags @@ -27,7 +26,7 @@ export function getArtifactTagsByEffectedPolicySelection( otherTags: string[] = [] ): string[] { if (selection.isGlobal) { - return [GLOBAL_POLICY_TAG, ...otherTags]; + return [GLOBAL_ARTIFACT_TAG, ...otherTags]; } const newTags = selection.selected.map((policy) => { return `policy:${policy.id}`; @@ -47,7 +46,7 @@ export function getEffectedPolicySelectionByTags( tags: string[], policies: PolicyData[] ): EffectedPolicySelection { - if (tags.find((tag) => tag === GLOBAL_POLICY_TAG)) { + if (tags.find((tag) => tag === GLOBAL_ARTIFACT_TAG)) { return { isGlobal: true, selected: [], @@ -71,7 +70,7 @@ export function getEffectedPolicySelectionByTags( } export function isGlobalPolicyEffected(tags?: string[]): boolean { - return tags !== undefined && tags.find((tag) => tag === GLOBAL_POLICY_TAG) !== undefined; + return tags !== undefined && tags.find((tag) => tag === GLOBAL_ARTIFACT_TAG) !== undefined; } /** diff --git a/x-pack/plugins/security_solution/public/management/pages/mocks/trusted_apps_http_mocks.ts b/x-pack/plugins/security_solution/public/management/pages/mocks/trusted_apps_http_mocks.ts index 45bb3841c0098..347f1dc088c10 100644 --- a/x-pack/plugins/security_solution/public/management/pages/mocks/trusted_apps_http_mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/mocks/trusted_apps_http_mocks.ts @@ -26,7 +26,6 @@ import { ResponseProvidersInterface, } from '../../../common/mock/endpoint/http_handler_mock_factory'; import { ExceptionsListItemGenerator } from '../../../../common/endpoint/data_generators/exceptions_list_item_generator'; -import { POLICY_REFERENCE_PREFIX } from '../../../../common/endpoint/service/trusted_apps/mapping'; import { getTrustedAppsListSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { fleetGetAgentPolicyListHttpMock, @@ -34,6 +33,7 @@ import { fleetGetEndpointPackagePolicyListHttpMock, FleetGetEndpointPackagePolicyListHttpMockInterface, } from './fleet_mocks'; +import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../common/endpoint/service/artifacts/constants'; interface FindExceptionListItemSchemaQueryParams extends Omit { @@ -67,8 +67,8 @@ export const trustedAppsGetListHttpMocks = data[2].tags = [ // IDs below are those generated by the `fleetGetEndpointPackagePolicyListHttpMock()` mock, // so if using in combination with that API mock, these should just "work" - `${POLICY_REFERENCE_PREFIX}ddf6570b-9175-4a6d-b288-61a09771c647`, - `${POLICY_REFERENCE_PREFIX}b8e616ae-44fc-4be7-846c-ce8fa5c082dd`, + `${BY_POLICY_ARTIFACT_TAG_PREFIX}ddf6570b-9175-4a6d-b288-61a09771c647`, + `${BY_POLICY_ARTIFACT_TAG_PREFIX}b8e616ae-44fc-4be7-846c-ce8fa5c082dd`, ]; return { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/mappers.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/mappers.ts index cab162fe30250..8069d18169dd1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/mappers.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/mappers.ts @@ -27,10 +27,8 @@ import { TrustedAppEntryTypes, UpdateTrustedApp, } from '../../../../../common/endpoint/types'; -import { - POLICY_REFERENCE_PREFIX, - tagsToEffectScope, -} from '../../../../../common/endpoint/service/trusted_apps/mapping'; +import { tagsToEffectScope } from '../../../../../common/endpoint/service/trusted_apps/mapping'; +import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../../common/endpoint/service/artifacts/constants'; type ConditionEntriesMap = { [K in ConditionEntryField]?: ConditionEntry }; type Mapping = { [K in T]: U }; @@ -177,9 +175,9 @@ const createEntryNested = (field: string, entries: NestedEntriesArray): EntryNes const effectScopeToTags = (effectScope: EffectScope) => { if (effectScope.type === 'policy') { - return effectScope.policies.map((policy) => `${POLICY_REFERENCE_PREFIX}${policy}`); + return effectScope.policies.map((policy) => `${BY_POLICY_ARTIFACT_TAG_PREFIX}${policy}`); } else { - return [`${POLICY_REFERENCE_PREFIX}all`]; + return [`${BY_POLICY_ARTIFACT_TAG_PREFIX}all`]; } }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index 7443e4b0d12a9..4808857849cb3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -301,7 +301,6 @@ describe('When on the Trusted Apps Page', () => { id: '05b5e350-0cad-4dc3-a61d-6e6796b0af39', comments: [], item_id: '2d95bec3-b48f-4db7-9622-a2b061cc031d', - meta: {}, namespace_type: 'agnostic', type: 'simple', }); 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 b61e850df2ef5..3a8616f5d3627 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 @@ -6,7 +6,7 @@ */ import { KibanaRequest, Logger } from 'src/core/server'; -import { CreateExceptionListItemOptions, ExceptionListClient } from '../../../lists/server'; +import { ExceptionListClient } from '../../../lists/server'; import { CasesClient, PluginStartContract as CasesPluginStartContract, @@ -35,11 +35,14 @@ import { EndpointAppContentServicesNotStartedError, } from './errors'; import { - EndpointFleetServicesFactory, + EndpointFleetServicesFactoryInterface, EndpointInternalFleetServicesInterface, EndpointScopedFleetServicesInterface, -} from './services/endpoint_fleet_services'; +} from './services/fleet/endpoint_fleet_services_factory'; import type { ListsServerExtensionRegistrar } from '../../../lists/server'; +import { registerListsPluginEndpointExtensionPoints } from '../lists_integration'; +import { EndpointAuthz } from '../../common/endpoint/types/authz'; +import { calculateEndpointAuthz } from '../../common/endpoint/service/authz'; export interface EndpointAppContextServiceSetupContract { securitySolutionRequestContextFactory: IRequestContextFactory; @@ -51,8 +54,10 @@ export type EndpointAppContextServiceStartContract = Partial< 'agentService' | 'packageService' | 'packagePolicyService' | 'agentPolicyService' > > & { + fleetAuthzService?: FleetStartContract['authz']; logger: Logger; endpointMetadataService: EndpointMetadataService; + endpointFleetServicesFactory: EndpointFleetServicesFactoryInterface; manifestManager?: ManifestManager; security: SecurityPluginStart; alerting: AlertsPluginStartContract; @@ -71,7 +76,7 @@ export type EndpointAppContextServiceStartContract = Partial< export class EndpointAppContextService { private setupDependencies: EndpointAppContextServiceSetupContract | null = null; private startDependencies: EndpointAppContextServiceStartContract | null = null; - private fleetServicesFactory: EndpointFleetServicesFactory | null = null; + private fleetServicesFactory: EndpointFleetServicesFactoryInterface | null = null; public security: SecurityPluginStart | undefined; public setup(dependencies: EndpointAppContextServiceSetupContract) { @@ -85,17 +90,7 @@ export class EndpointAppContextService { this.startDependencies = dependencies; this.security = dependencies.security; - - // let's try to avoid turning off eslint's Forbidden non-null assertion rule - const { agentService, agentPolicyService, packagePolicyService, packageService } = - dependencies as Required; - - this.fleetServicesFactory = new EndpointFleetServicesFactory({ - agentService, - agentPolicyService, - packagePolicyService, - packageService, - }); + this.fleetServicesFactory = dependencies.endpointFleetServicesFactory; if (dependencies.registerIngestCallback && dependencies.manifestManager) { dependencies.registerIngestCallback( @@ -124,13 +119,7 @@ export class EndpointAppContextService { if (this.startDependencies.registerListsServerExtension) { const { registerListsServerExtension } = this.startDependencies; - registerListsServerExtension({ - type: 'exceptionsListPreCreateItem', - callback: async (arg: CreateExceptionListItemOptions) => { - // this.startDependencies?.logger.info('exceptionsListPreCreateItem called!'); - return arg; - }, - }); + registerListsPluginEndpointExtensionPoints(registerListsServerExtension, this); } } @@ -140,6 +129,19 @@ export class EndpointAppContextService { return this.startDependencies?.config.experimentalFeatures; } + private getFleetAuthzService(): FleetStartContract['authz'] { + if (!this.startDependencies?.fleetAuthzService) { + throw new EndpointAppContentServicesNotStartedError(); + } + + return this.startDependencies.fleetAuthzService; + } + + public async getEndpointAuthz(request: KibanaRequest): Promise { + const fleetAuthz = await this.getFleetAuthzService().fromRequest(request); + return calculateEndpointAuthz(this.getLicenseService(), fleetAuthz); + } + public getEndpointMetadataService(): EndpointMetadataService { if (this.startDependencies == null) { throw new EndpointAppContentServicesNotStartedError(); @@ -198,4 +200,11 @@ export class EndpointAppContextService { } return this.startDependencies.cases.getCasesClientWithRequest(req); } + + public getExceptionListsClient(): ExceptionListClient { + if (!this.startDependencies?.exceptionListsClient) { + throw new EndpointAppContentServicesNotStartedError(); + } + return this.startDependencies.exceptionListsClient; + } } diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 4da108859691f..fa22869b391ab 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -29,7 +29,6 @@ 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 { LicenseService } from '../../common/license'; import { SecuritySolutionRequestHandlerContext } from '../types'; import { parseExperimentalConfigValue } from '../../common/experimental_features'; // A TS error (TS2403) is thrown when attempting to export the mock function below from Cases @@ -45,6 +44,8 @@ import { createMockClients } from '../lib/detection_engine/routes/__mocks__/requ import { createEndpointMetadataServiceTestContextMock } from './services/metadata/mocks'; import type { EndpointAuthz } from '../../common/endpoint/types/authz'; +import { EndpointFleetServicesFactory } from './services/fleet'; +import { createLicenseServiceMock } from '../../common/license/mocks'; /** * Creates a mocked EndpointAppContext. @@ -102,12 +103,22 @@ export const createMockEndpointAppContextServiceStartContract = const agentService = createMockAgentService(); const agentPolicyService = createMockAgentPolicyService(); const packagePolicyService = createPackagePolicyServiceMock(); + const packageService = createMockPackageService(); const endpointMetadataService = new EndpointMetadataService( savedObjectsStart, agentPolicyService, packagePolicyService, logger ); + const endpointFleetServicesFactory = new EndpointFleetServicesFactory( + { + packageService, + packagePolicyService, + agentPolicyService, + agentService, + }, + savedObjectsStart + ); packagePolicyService.list.mockImplementation(async (_, options) => { return { @@ -122,14 +133,16 @@ export const createMockEndpointAppContextServiceStartContract = agentService, agentPolicyService, endpointMetadataService, + endpointFleetServicesFactory, packagePolicyService, logger, - packageService: createMockPackageService(), + packageService, + fleetAuthzService: createFleetAuthzServiceMock(), manifestManager: getManifestManagerMock(), security: securityMock.createStart(), alerting: alertsMock.createStart(), config, - licenseService: new LicenseService(), + licenseService: createLicenseServiceMock(), registerIngestCallback: jest.fn< ReturnType, Parameters @@ -141,18 +154,21 @@ export const createMockEndpointAppContextServiceStartContract = }; }; +export const createFleetAuthzServiceMock = (): jest.Mocked => { + return { + fromRequest: jest.fn(async (_) => createFleetAuthzMock()), + }; +}; + /** - * Creates a mock IndexPatternService for use in tests that need to interact with the Fleet's - * ESIndexPatternService. + * Creates the Fleet Start contract mock return by the Fleet Plugin * * @param indexPattern a string index pattern to return when called by a test * @returns the same value as `indexPattern` parameter */ export const createMockFleetStartContract = (indexPattern: string): FleetStartContract => { return { - authz: { - fromRequest: jest.fn().mockResolvedValue(createFleetAuthzMock()), - }, + authz: createFleetAuthzServiceMock(), fleetSetupCompleted: jest.fn().mockResolvedValue(undefined), esIndexPatternService: { getESIndexPattern: jest.fn().mockResolvedValue(indexPattern), 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 4b1ea1fe9efaa..02d13c7c057d1 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 @@ -42,7 +42,7 @@ import { ENDPOINT_DEFAULT_PAGE_SIZE, METADATA_TRANSFORMS_PATTERN, } from '../../../../common/endpoint/constants'; -import { EndpointFleetServicesInterface } from '../../services/endpoint_fleet_services'; +import { EndpointFleetServicesInterface } from '../../services/fleet/endpoint_fleet_services_factory'; export interface MetadataRequestContext { esClient?: IScopedClusterClient; 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 527afe23b694d..9ad26443cf0d5 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 @@ -11,11 +11,7 @@ import { createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, } from '../../mocks'; -import { - createMockAgentClient, - createMockAgentService, - createPackagePolicyServiceMock, -} from '../../../../../fleet/server/mocks'; +import { createMockAgentClient, createMockAgentService } from '../../../../../fleet/server/mocks'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../fleet/common'; import { getHostPolicyResponseHandler, @@ -251,10 +247,20 @@ describe('test policy response handler', () => { let policyHandler: ReturnType; beforeEach(() => { + const endpointAppContextServiceStartContract = + createMockEndpointAppContextServiceStartContract(); + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockResponse = httpServerMock.createResponseFactory(); - mockPackagePolicyService = createPackagePolicyServiceMock(); + + if (endpointAppContextServiceStartContract.packagePolicyService) { + mockPackagePolicyService = + endpointAppContextServiceStartContract.packagePolicyService as jest.Mocked; + } else { + expect(endpointAppContextServiceStartContract.packagePolicyService).toBeTruthy(); + } + mockPackagePolicyService.list.mockImplementation(() => { return Promise.resolve({ items: [], @@ -265,10 +271,7 @@ describe('test policy response handler', () => { }); endpointAppContextService = new EndpointAppContextService(); endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); - endpointAppContextService.start({ - ...createMockEndpointAppContextServiceStartContract(), - ...{ packagePolicyService: mockPackagePolicyService }, - }); + endpointAppContextService.start(endpointAppContextServiceStartContract); policyHandler = getPolicyListHandler({ logFactory: loggingSystemMock.create(), service: endpointAppContextService, @@ -289,6 +292,7 @@ describe('test policy response handler', () => { mockRequest, mockResponse ); + expect(mockPackagePolicyService.list).toHaveBeenCalled(); expect(mockPackagePolicyService.list.mock.calls[0][1]).toEqual({ kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`, perPage: undefined, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/endpoint_fleet_services.ts b/x-pack/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.ts similarity index 81% rename from x-pack/plugins/security_solution/server/endpoint/services/endpoint_fleet_services.ts rename to x-pack/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.ts index 915070a9b064f..7ab263705eaaf 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/endpoint_fleet_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.ts @@ -5,14 +5,15 @@ * 2.0. */ -import { KibanaRequest } from 'kibana/server'; +import { KibanaRequest, SavedObjectsClientContract, SavedObjectsServiceStart } from 'kibana/server'; import type { AgentClient, AgentPolicyServiceInterface, FleetStartContract, PackagePolicyServiceInterface, PackageClient, -} from '../../../../fleet/server'; +} from '../../../../../fleet/server'; +import { createInternalReadonlySoClient } from '../../utils/create_internal_readonly_so_client'; export interface EndpointFleetServicesFactoryInterface { asScoped(req: KibanaRequest): EndpointScopedFleetServicesInterface; @@ -25,7 +26,8 @@ export class EndpointFleetServicesFactory implements EndpointFleetServicesFactor private readonly fleetDependencies: Pick< FleetStartContract, 'agentService' | 'packageService' | 'packagePolicyService' | 'agentPolicyService' - > + >, + private savedObjectsStart: SavedObjectsServiceStart ) {} asScoped(req: KibanaRequest): EndpointScopedFleetServicesInterface { @@ -61,6 +63,7 @@ export class EndpointFleetServicesFactory implements EndpointFleetServicesFactor packagePolicy, asScoped: this.asScoped.bind(this), + internalReadonlySoClient: createInternalReadonlySoClient(this.savedObjectsStart), }; } } @@ -87,4 +90,9 @@ export interface EndpointInternalFleetServicesInterface extends EndpointFleetSer * get scoped endpoint fleet services instance */ asScoped: EndpointFleetServicesFactoryInterface['asScoped']; + + /** + * An internal SO client (readonly) that can be used with the Fleet services that require it + */ + internalReadonlySoClient: SavedObjectsClientContract; } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/fleet/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/fleet/index.ts new file mode 100644 index 0000000000000..6e1088a21ee02 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/fleet/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 * from './endpoint_fleet_services_factory'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts index 5251992a5d3d4..857b9ca18163c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts @@ -56,7 +56,7 @@ import { getAllEndpointPackagePolicies } from '../../routes/metadata/support/end import { getAgentStatus } from '../../../../../fleet/common/services/agent_status'; import { GetMetadataListRequestQuery } from '../../../../common/endpoint/schema/metadata'; import { EndpointError } from '../../../../common/endpoint/errors'; -import { EndpointFleetServicesInterface } from '../endpoint_fleet_services'; +import { EndpointFleetServicesInterface } from '../fleet/endpoint_fleet_services_factory'; type AgentPolicyWithPackagePolicies = Omit & { package_policies: PackagePolicy[]; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts index 3cd368d35b657..599f153e19e05 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts @@ -21,7 +21,7 @@ import { AgentPolicyServiceInterface, AgentService } from '../../../../../fleet/ import { EndpointFleetServicesFactory, EndpointInternalFleetServicesInterface, -} from '../endpoint_fleet_services'; +} from '../fleet/endpoint_fleet_services_factory'; const createCustomizedPackagePolicyService = () => { const service = createPackagePolicyServiceMock(); @@ -62,12 +62,15 @@ export const createEndpointMetadataServiceTestContextMock = ( .create() .get() ): EndpointMetadataServiceTestContextMock => { - const fleetServices = new EndpointFleetServicesFactory({ - agentService, - packageService, - packagePolicyService, - agentPolicyService, - }).asInternalUser(); + const fleetServices = new EndpointFleetServicesFactory( + { + agentService, + packageService, + packagePolicyService, + agentPolicyService, + }, + savedObjectsStart + ).asInternalUser(); const endpointMetadataService = new EndpointMetadataService( savedObjectsStart, diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_create_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_create_handler.ts new file mode 100644 index 0000000000000..08a6f80162d03 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_create_handler.ts @@ -0,0 +1,26 @@ +/* + * 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 { + CreateExceptionListItemOptions, + ExceptionsListPreCreateItemServerExtension, +} from '../../../../../lists/server'; +import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; +import { TrustedAppValidator } from '../validators'; + +export const getExceptionsPreCreateItemHandler = ( + endpointAppContext: EndpointAppContextService +): ExceptionsListPreCreateItemServerExtension['callback'] => { + return async function ({ data, context: { request } }): Promise { + // Validate trusted apps + if (TrustedAppValidator.isTrustedApp(data)) { + return new TrustedAppValidator(endpointAppContext, request).validatePreCreateItem(data); + } + + return data; + }; +}; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts new file mode 100644 index 0000000000000..4c162bb03a5e9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts @@ -0,0 +1,43 @@ +/* + * 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 { + ExceptionsListPreUpdateItemServerExtension, + UpdateExceptionListItemOptions, +} from '../../../../../lists/server'; +import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; +import { TrustedAppValidator } from '../validators'; + +export const getExceptionsPreUpdateItemHandler = ( + endpointAppContextService: EndpointAppContextService +): ExceptionsListPreUpdateItemServerExtension['callback'] => { + return async function ({ data, context: { request } }): Promise { + const currentSavedItem = await endpointAppContextService + .getExceptionListsClient() + .getExceptionListItem({ + id: data.id, + itemId: data.itemId, + namespaceType: data.namespaceType, + }); + + // We don't want to `throw` here becuase we don't know for sure that the item is one we care about. + // So we just return the data and the Lists plugin will likely error out because it can't find the item + if (!currentSavedItem) { + return data; + } + + // Validate trusted apps + if (TrustedAppValidator.isTrustedApp({ listId: currentSavedItem.list_id })) { + return new TrustedAppValidator(endpointAppContextService, request).validatePreUpdateItem( + data, + currentSavedItem + ); + } + + return data; + }; +}; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/register_endpoint_extension_points.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/register_endpoint_extension_points.ts new file mode 100644 index 0000000000000..bc0c59f44be13 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/register_endpoint_extension_points.ts @@ -0,0 +1,28 @@ +/* + * 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 { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services'; +import { getExceptionsPreCreateItemHandler } from './handlers/exceptions_pre_create_handler'; +import { getExceptionsPreUpdateItemHandler } from './handlers/exceptions_pre_update_handler'; +import type { ListsServerExtensionRegistrar } from '../../../../lists/server'; + +export const registerListsPluginEndpointExtensionPoints = ( + registerListsExtensionPoint: ListsServerExtensionRegistrar, + endpointAppContextService: EndpointAppContextService +): void => { + // PRE-CREATE handler + registerListsExtensionPoint({ + type: 'exceptionsListPreCreateItem', + callback: getExceptionsPreCreateItemHandler(endpointAppContextService), + }); + + // PRE-UPDATE handler + registerListsExtensionPoint({ + type: 'exceptionsListPreUpdateItem', + callback: getExceptionsPreUpdateItemHandler(endpointAppContextService), + }); +}; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/types.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/types.ts new file mode 100644 index 0000000000000..4b00bafafc967 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/types.ts @@ -0,0 +1,19 @@ +/* + * 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 { CreateExceptionListItemOptions } from '../../../../lists/server'; + +/** + * An Exception Like item is a structure used internally by several of the Exceptions api/service in that + * the keys are camelCased. Because different methods of the ExceptionListClient have slightly different + * structures, this one attempt to normalize the properties we care about here that can be found across + * those service methods. + */ +export type ExceptionItemLikeOptions = Pick< + CreateExceptionListItemOptions, + 'osTypes' | 'tags' | 'description' | 'name' | 'entries' | 'namespaceType' +> & { listId?: string }; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts new file mode 100644 index 0000000000000..728e3b8559ed3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts @@ -0,0 +1,187 @@ +/* + * 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 { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; +import { + createMockEndpointAppContextServiceSetupContract, + createMockEndpointAppContextServiceStartContract, +} from '../../../endpoint/mocks'; +import { BaseValidatorMock, createExceptionItemLikeOptionsMock } from './mocks'; +import { EndpointArtifactExceptionValidationError } from './errors'; +import { httpServerMock } from '../../../../../../../src/core/server/mocks'; +import { createFleetAuthzMock, PackagePolicy } from '../../../../../fleet/common'; +import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; +import { ExceptionItemLikeOptions } from '../types'; +import { + BY_POLICY_ARTIFACT_TAG_PREFIX, + GLOBAL_ARTIFACT_TAG, +} from '../../../../common/endpoint/service/artifacts'; + +describe('When using Artifacts Exceptions BaseValidator', () => { + let endpointAppContextServices: EndpointAppContextService; + let kibanaRequest: ReturnType; + let exceptionLikeItem: ExceptionItemLikeOptions; + let validator: BaseValidatorMock; + let packagePolicyService: jest.Mocked; + let initValidator: (withNoAuth?: boolean, withBasicLicense?: boolean) => BaseValidatorMock; + + beforeEach(() => { + kibanaRequest = httpServerMock.createKibanaRequest(); + exceptionLikeItem = createExceptionItemLikeOptionsMock(); + + const servicesStart = createMockEndpointAppContextServiceStartContract(); + + packagePolicyService = + servicesStart.packagePolicyService as jest.Mocked; + + endpointAppContextServices = new EndpointAppContextService(); + endpointAppContextServices.setup(createMockEndpointAppContextServiceSetupContract()); + endpointAppContextServices.start(servicesStart); + + initValidator = (withNoAuth: boolean = false, withBasicLicense = false) => { + if (withNoAuth) { + const fleetAuthz = createFleetAuthzMock(); + fleetAuthz.fleet.all = false; + (servicesStart.fleetAuthzService?.fromRequest as jest.Mock).mockResolvedValue(fleetAuthz); + } + + if (withBasicLicense) { + (servicesStart.licenseService.isPlatinumPlus as jest.Mock).mockResolvedValue(false); + } + + validator = new BaseValidatorMock(endpointAppContextServices, kibanaRequest); + + return validator; + }; + }); + + it('should use default endpoint authz (no access) when `request` is not provided', async () => { + const baseValidator = new BaseValidatorMock(endpointAppContextServices); + + await expect(baseValidator._isAllowedToCreateArtifactsByPolicy()).resolves.toBe(false); + await expect(baseValidator._validateCanManageEndpointArtifacts()).rejects.toBeInstanceOf( + EndpointArtifactExceptionValidationError + ); + }); + + it('should validate is allowed to manage endpoint artifacts', async () => { + await expect(initValidator()._validateCanManageEndpointArtifacts()).resolves.toBeUndefined(); + }); + + it('should throw if not allowed to manage endpoint artifacts', async () => { + await expect(initValidator(true)._validateCanManageEndpointArtifacts()).rejects.toBeInstanceOf( + EndpointArtifactExceptionValidationError + ); + }); + + it('should validate basic artifact data', async () => { + await expect(initValidator()._validateBasicData(exceptionLikeItem)).resolves.toBeUndefined(); + }); + + it.each([ + [ + 'name is empty', + () => { + exceptionLikeItem.name = ''; + }, + ], + [ + 'namespace is not agnostic', + () => { + exceptionLikeItem.namespaceType = 'single'; + }, + ], + [ + 'osTypes has more than 1 value', + () => { + exceptionLikeItem.osTypes = ['macos', 'linux']; + }, + ], + [ + 'osType has invalid value', + () => { + exceptionLikeItem.osTypes = ['xunil' as 'linux']; + }, + ], + ])('should throw if %s', async (_, setupData) => { + setupData(); + + await expect(initValidator()._validateBasicData(exceptionLikeItem)).rejects.toBeInstanceOf( + EndpointArtifactExceptionValidationError + ); + }); + + it('should validate is allowed to create artifacts by policy', async () => { + await expect( + initValidator()._validateCanCreateByPolicyArtifacts(exceptionLikeItem) + ).resolves.toBeUndefined(); + }); + + it('should throw if not allowed to create artifacts by policy', async () => { + await expect( + initValidator(false, true)._validateCanCreateByPolicyArtifacts(exceptionLikeItem) + ).rejects.toBeInstanceOf(EndpointArtifactExceptionValidationError); + }); + + it('should validate policy ids for by policy artifacts', async () => { + packagePolicyService.getByIDs.mockResolvedValue([ + { + id: '123', + version: '123', + } as PackagePolicy, + ]); + + await expect(initValidator()._validateByPolicyItem(exceptionLikeItem)).resolves.toBeUndefined(); + }); + + it('should throw if policy ids for by policy artifacts are not valid', async () => { + packagePolicyService.getByIDs.mockResolvedValue([ + { + id: '123', + version: undefined, + } as PackagePolicy, + ]); + + await expect(initValidator()._validateByPolicyItem(exceptionLikeItem)).rejects.toBeInstanceOf( + EndpointArtifactExceptionValidationError + ); + }); + + it.each([ + ['no policies (unassigned)', () => createExceptionItemLikeOptionsMock({ tags: [] })], + [ + 'different policy', + () => createExceptionItemLikeOptionsMock({ tags: [`${BY_POLICY_ARTIFACT_TAG_PREFIX}:456`] }), + ], + [ + 'additional policies', + () => + createExceptionItemLikeOptionsMock({ + tags: [`${BY_POLICY_ARTIFACT_TAG_PREFIX}:123`, `${BY_POLICY_ARTIFACT_TAG_PREFIX}:456`], + }), + ], + ])( + 'should return `true` when `wasByPolicyEffectScopeChanged()` is called with: %s', + (_, getUpdated) => { + expect(initValidator()._wasByPolicyEffectScopeChanged(getUpdated(), exceptionLikeItem)).toBe( + true + ); + } + ); + + it.each([ + ['identical data', () => createExceptionItemLikeOptionsMock()], + [ + 'scope changed to all', + () => createExceptionItemLikeOptionsMock({ tags: [GLOBAL_ARTIFACT_TAG] }), + ], + ])('should return `false` when `wasByPolicyEffectScopeChanged()` with: %s', (_, getUpdated) => { + expect(initValidator()._wasByPolicyEffectScopeChanged(getUpdated(), exceptionLikeItem)).toBe( + false + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts new file mode 100644 index 0000000000000..d320a2ecc8aef --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts @@ -0,0 +1,164 @@ +/* + * 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 { KibanaRequest } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { isEqual } from 'lodash/fp'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; +import { ExceptionItemLikeOptions } from '../types'; +import { getEndpointAuthzInitialState } from '../../../../common/endpoint/service/authz'; +import { + getPolicyIdsFromArtifact, + isArtifactByPolicy, +} from '../../../../common/endpoint/service/artifacts'; +import { OperatingSystem } from '../../../../common/endpoint/types'; +import { EndpointArtifactExceptionValidationError } from './errors'; + +const BasicEndpointExceptionDataSchema = schema.object( + { + // must have a name + name: schema.string({ minLength: 1, maxLength: 256 }), + + description: schema.maybe(schema.string({ minLength: 0, maxLength: 256, defaultValue: '' })), + + // We only support agnostic entries + namespaceType: schema.literal('agnostic'), + + // only one OS per entry + osTypes: schema.arrayOf( + schema.oneOf([ + schema.literal(OperatingSystem.WINDOWS), + schema.literal(OperatingSystem.LINUX), + schema.literal(OperatingSystem.MAC), + ]), + { minSize: 1, maxSize: 1 } + ), + }, + // Because we are only validating some fields from the Exception Item, we set `unknowns` to `ignore` here + { unknowns: 'ignore' } +); + +/** + * Provides base methods for doing validation that apply across endpoint exception entries + */ +export class BaseValidator { + private readonly endpointAuthzPromise: ReturnType; + + constructor( + protected readonly endpointAppContext: EndpointAppContextService, + /** + * Request is optional only because it needs to be optional in the Lists ExceptionListClient + */ + private readonly request?: KibanaRequest + ) { + if (this.request) { + this.endpointAuthzPromise = this.endpointAppContext.getEndpointAuthz(this.request); + } else { + this.endpointAuthzPromise = Promise.resolve(getEndpointAuthzInitialState()); + } + } + + protected isItemByPolicy(item: ExceptionItemLikeOptions): boolean { + return isArtifactByPolicy(item); + } + + protected async isAllowedToCreateArtifactsByPolicy(): Promise { + return (await this.endpointAuthzPromise).canCreateArtifactsByPolicy; + } + + protected async validateCanManageEndpointArtifacts(): Promise { + if (!(await this.endpointAuthzPromise).canAccessEndpointManagement) { + throw new EndpointArtifactExceptionValidationError('Endpoint authorization failure', 403); + } + } + + /** + * validates some basic common data that can be found across all endpoint exceptions + * @param item + * @protected + */ + protected async validateBasicData(item: ExceptionItemLikeOptions) { + try { + BasicEndpointExceptionDataSchema.validate(item); + } catch (error) { + throw new EndpointArtifactExceptionValidationError(error.message); + } + } + + protected async validateCanCreateByPolicyArtifacts( + item: ExceptionItemLikeOptions + ): Promise { + if (this.isItemByPolicy(item) && !(await this.isAllowedToCreateArtifactsByPolicy())) { + throw new EndpointArtifactExceptionValidationError( + 'Your license level does not allow create/update of by policy artifacts', + 403 + ); + } + } + + /** + * Validates that by-policy artifacts is permitted and that each policy referenced in the item is valid + * @protected + */ + protected async validateByPolicyItem(item: ExceptionItemLikeOptions): Promise { + if (this.isItemByPolicy(item)) { + const { packagePolicy, internalReadonlySoClient } = + this.endpointAppContext.getInternalFleetServices(); + const policyIds = getPolicyIdsFromArtifact(item); + + if (policyIds.length === 0) { + return; + } + + const policiesFromFleet = await packagePolicy.getByIDs(internalReadonlySoClient, policyIds); + + if (!policiesFromFleet) { + throw new EndpointArtifactExceptionValidationError( + `invalid policy ids: ${policyIds.join(', ')}` + ); + } + + const invalidPolicyIds = policiesFromFleet + .filter((policy) => policy.version === undefined) + .map((policy) => policy.id); + + if (invalidPolicyIds.length) { + throw new EndpointArtifactExceptionValidationError( + `invalid policy ids: ${invalidPolicyIds.join(', ')}` + ); + } + } + } + + /** + * If the item being updated is `by policy`, method validates if anyting was changes in regard to + * the effected scope of the by policy settings. + * + * @param updatedItem + * @param currentItem + * @protected + */ + protected wasByPolicyEffectScopeChanged( + updatedItem: ExceptionItemLikeOptions, + currentItem: Pick + ): boolean { + // if global, then return. Nothing to validate and setting the trusted app to global is allowed + if (!this.isItemByPolicy(updatedItem)) { + return false; + } + + if (updatedItem.tags) { + return !isEqual( + getPolicyIdsFromArtifact({ tags: updatedItem.tags }), + getPolicyIdsFromArtifact(currentItem) + ); + } + + return false; + } +} diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/errors.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/errors.ts new file mode 100644 index 0000000000000..69a8d9c5914de --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/errors.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 { ListsErrorWithStatusCode } from '../../../../../lists/server'; + +export class EndpointArtifactExceptionValidationError extends ListsErrorWithStatusCode { + constructor(message: string, statusCode: number = 400) { + super(`EndpointArtifactError: ${message}`, statusCode); + } +} diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/index.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/index.ts new file mode 100644 index 0000000000000..be26c31e2e155 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/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 { TrustedAppValidator } from './trusted_app_validator'; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/mocks.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/mocks.ts new file mode 100644 index 0000000000000..97a29aee962e5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/mocks.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { BaseValidator } from './base_validator'; +import { ExceptionItemLikeOptions } from '../types'; +import { listMock } from '../../../../../lists/server/mocks'; +import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../common/endpoint/service/artifacts'; + +/** + * Exposes all `protected` methods of `BaseValidator` by prefixing them with an underscore. + */ +export class BaseValidatorMock extends BaseValidator { + _isItemByPolicy(item: ExceptionItemLikeOptions): boolean { + return this.isItemByPolicy(item); + } + + async _isAllowedToCreateArtifactsByPolicy(): Promise { + return this.isAllowedToCreateArtifactsByPolicy(); + } + + async _validateCanManageEndpointArtifacts(): Promise { + return this.validateCanManageEndpointArtifacts(); + } + + async _validateBasicData(item: ExceptionItemLikeOptions) { + return this.validateBasicData(item); + } + + async _validateCanCreateByPolicyArtifacts(item: ExceptionItemLikeOptions): Promise { + return this.validateCanCreateByPolicyArtifacts(item); + } + + async _validateByPolicyItem(item: ExceptionItemLikeOptions): Promise { + return this.validateByPolicyItem(item); + } + + _wasByPolicyEffectScopeChanged( + updatedItem: ExceptionItemLikeOptions, + currentItem: Pick + ): boolean { + return this.wasByPolicyEffectScopeChanged(updatedItem, currentItem); + } +} + +export const createExceptionItemLikeOptionsMock = ( + overrides: Partial = {} +): ExceptionItemLikeOptions => { + return { + ...listMock.getCreateExceptionListItemOptionsMock(), + namespaceType: 'agnostic', + osTypes: ['windows'], + tags: [`${BY_POLICY_ARTIFACT_TAG_PREFIX}123`], + ...overrides, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts new file mode 100644 index 0000000000000..26208661b4b6b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts @@ -0,0 +1,224 @@ +/* + * 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 { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { schema, TypeOf } from '@kbn/config-schema'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { BaseValidator } from './base_validator'; +import { ExceptionItemLikeOptions } from '../types'; +import { + CreateExceptionListItemOptions, + UpdateExceptionListItemOptions, +} from '../../../../../lists/server'; +import { + ConditionEntry, + OperatingSystem, + TrustedAppEntryTypes, +} from '../../../../common/endpoint/types'; +import { + getDuplicateFields, + isValidHash, +} from '../../../../common/endpoint/service/trusted_apps/validations'; +import { EndpointArtifactExceptionValidationError } from './errors'; + +const ProcessHashField = schema.oneOf([ + schema.literal('process.hash.md5'), + schema.literal('process.hash.sha1'), + schema.literal('process.hash.sha256'), +]); +const ProcessExecutablePath = schema.literal('process.executable.caseless'); +const ProcessCodeSigner = schema.literal('process.Ext.code_signature'); + +const ConditionEntryTypeSchema = schema.conditional( + schema.siblingRef('field'), + ProcessExecutablePath, + schema.oneOf([schema.literal('match'), schema.literal('wildcard')]), + schema.literal('match') +); +const ConditionEntryOperatorSchema = schema.literal('included'); + +type ConditionEntryFieldAllowedType = + | TypeOf + | TypeOf + | TypeOf; + +type TrustedAppConditionEntry< + T extends ConditionEntryFieldAllowedType = ConditionEntryFieldAllowedType +> = + | { + field: T; + type: TrustedAppEntryTypes; + operator: 'included'; + value: string; + } + | TypeOf; + +/* + * A generic Entry schema to be used for a specific entry schema depending on the OS + */ +const CommonEntrySchema = { + field: schema.oneOf([ProcessHashField, ProcessExecutablePath]), + type: ConditionEntryTypeSchema, + operator: ConditionEntryOperatorSchema, + // If field === HASH then validate hash with custom method, else validate string with minLength = 1 + value: schema.conditional( + schema.siblingRef('field'), + ProcessHashField, + schema.string({ + validate: (hash: string) => (isValidHash(hash) ? undefined : `invalid hash value [${hash}]`), + }), + schema.conditional( + schema.siblingRef('field'), + ProcessExecutablePath, + schema.string({ + validate: (pathValue: string) => + pathValue.length > 0 ? undefined : `invalid path value [${pathValue}]`, + }), + schema.string({ + validate: (signerValue: string) => + signerValue.length > 0 ? undefined : `invalid signer value [${signerValue}]`, + }) + ) + ), +}; + +// Windows Signer entries use a Nested field that checks to ensure +// that the certificate is trusted +const WindowsSignerEntrySchema = schema.object({ + type: schema.literal('nested'), + field: ProcessCodeSigner, + entries: schema.arrayOf( + schema.oneOf([ + schema.object({ + field: schema.literal('trusted'), + value: schema.literal('true'), + type: schema.literal('match'), + operator: schema.literal('included'), + }), + schema.object({ + field: schema.literal('subject_name'), + value: schema.string({ minLength: 1 }), + type: schema.literal('match'), + operator: schema.literal('included'), + }), + ]), + { minSize: 2, maxSize: 2 } + ), +}); + +const WindowsEntrySchema = schema.oneOf([ + WindowsSignerEntrySchema, + schema.object({ + ...CommonEntrySchema, + field: schema.oneOf([ProcessHashField, ProcessExecutablePath]), + }), +]); + +const LinuxEntrySchema = schema.object({ + ...CommonEntrySchema, +}); + +const MacEntrySchema = schema.object({ + ...CommonEntrySchema, +}); + +const entriesSchemaOptions = { + minSize: 1, + validate(entries: TrustedAppConditionEntry[]) { + const dups = getDuplicateFields(entries as ConditionEntry[]); + return dups.map((field) => `Duplicated entry: ${field}`).join(', ') || undefined; + }, +}; + +/* + * Entities array schema depending on Os type using schema.conditional. + * If OS === WINDOWS then use Windows schema, + * else if OS === LINUX then use Linux schema, + * else use Mac schema + * + * The validate function checks there is no duplicated entry inside the array + */ +const EntriesSchema = schema.conditional( + schema.contextRef('os'), + OperatingSystem.WINDOWS, + schema.arrayOf(WindowsEntrySchema, entriesSchemaOptions), + schema.conditional( + schema.contextRef('os'), + OperatingSystem.LINUX, + schema.arrayOf(LinuxEntrySchema, entriesSchemaOptions), + schema.arrayOf(MacEntrySchema, entriesSchemaOptions) + ) +); + +/** + * Schema to validate Trusted Apps data for create and update. + * When called, it must be given an `context` with a `os` property set + * + * @example + * + * TrustedAppDataSchema.validate(item, { os: 'windows' }); + */ +const TrustedAppDataSchema = schema.object( + { + entries: EntriesSchema, + }, + + // Because we are only validating some fields from the Exception Item, we set `unknowns` to `ignore` here + { unknowns: 'ignore' } +); + +export class TrustedAppValidator extends BaseValidator { + static isTrustedApp(item: { listId: string }): boolean { + return item.listId === ENDPOINT_TRUSTED_APPS_LIST_ID; + } + + async validatePreCreateItem( + item: CreateExceptionListItemOptions + ): Promise { + await this.validateCanManageEndpointArtifacts(); + await this.validateTrustedAppData(item); + await this.validateCanCreateByPolicyArtifacts(item); + await this.validateByPolicyItem(item); + + return item; + } + + async validatePreUpdateItem( + _updatedItem: UpdateExceptionListItemOptions, + currentItem: ExceptionListItemSchema + ): Promise { + const updatedItem = _updatedItem as ExceptionItemLikeOptions; + + await this.validateCanManageEndpointArtifacts(); + await this.validateTrustedAppData(updatedItem); + + try { + await this.validateCanCreateByPolicyArtifacts(updatedItem); + } catch (noByPolicyAuthzError) { + // Not allowed to create/update by policy data. Validate that the effective scope of the item + // remained unchanged with this update or was set to `global` (only allowed update). If not, + // then throw the validation error that was catch'ed + if (this.wasByPolicyEffectScopeChanged(updatedItem, currentItem)) { + throw noByPolicyAuthzError; + } + } + + await this.validateByPolicyItem(updatedItem); + + return updatedItem as UpdateExceptionListItemOptions; + } + + private async validateTrustedAppData(item: ExceptionItemLikeOptions): Promise { + await this.validateBasicData(item); + + try { + TrustedAppDataSchema.validate(item, { os: item.osTypes[0] }); + } catch (error) { + throw new EndpointArtifactExceptionValidationError(error.message); + } + } +} diff --git a/x-pack/plugins/security_solution/server/lists_integration/index.ts b/x-pack/plugins/security_solution/server/lists_integration/index.ts new file mode 100644 index 0000000000000..19f30492d0cb5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/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 { registerListsPluginEndpointExtensionPoints } from './endpoint/register_endpoint_extension_points'; diff --git a/x-pack/plugins/security_solution/server/lists_integration/jest.config.js b/x-pack/plugins/security_solution/server/lists_integration/jest.config.js new file mode 100644 index 0000000000000..0831c87ee59d4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/jest.config.js @@ -0,0 +1,26 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/server/lists_integration'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/server/lists_integration', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/security_solution/server/lists_integration/**/*.{ts,tsx}', + ], + // See: https://github.com/elastic/kibana/issues/117255, the moduleNameMapper creates mocks to avoid memory leaks from kibana core. + moduleNameMapper: { + 'core/server$': '/x-pack/plugins/security_solution/server/__mocks__/core.mock.ts', + 'task_manager/server$': + '/x-pack/plugins/security_solution/server/__mocks__/task_manager.mock.ts', + 'alerting/server$': '/x-pack/plugins/security_solution/server/__mocks__/alert.mock.ts', + 'actions/server$': '/x-pack/plugins/security_solution/server/__mocks__/action.mock.ts', + }, +}; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index a8e6d49a994c0..91118d0ef4e89 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -90,6 +90,7 @@ import type { PluginInitializerContext, } from './plugin_contract'; import { alertsFieldMap, rulesFieldMap } from '../common/field_maps'; +import { EndpointFleetServicesFactory } from './endpoint/services/fleet'; export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract'; @@ -394,19 +395,31 @@ export class Plugin implements ISecuritySolutionPlugin { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const exceptionListClient = this.lists!.getExceptionListClient(savedObjectsClient, 'kibana'); + const { authz, agentService, packageService, packagePolicyService, agentPolicyService } = + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + plugins.fleet!; + this.endpointAppContextService.start({ - agentService: plugins.fleet?.agentService, - packageService: plugins.fleet?.packageService, - packagePolicyService: plugins.fleet?.packagePolicyService, - agentPolicyService: plugins.fleet?.agentPolicyService, + fleetAuthzService: authz, + agentService, + packageService, + packagePolicyService, + agentPolicyService, endpointMetadataService: new EndpointMetadataService( core.savedObjects, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - plugins.fleet?.agentPolicyService!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - plugins.fleet?.packagePolicyService!, + agentPolicyService, + packagePolicyService, logger ), + endpointFleetServicesFactory: new EndpointFleetServicesFactory( + { + agentService, + packageService, + packagePolicyService, + agentPolicyService, + }, + core.savedObjects + ), security: plugins.security, alerting: plugins.alerting, config: this.config, diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts b/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts new file mode 100644 index 0000000000000..093e55216b9af --- /dev/null +++ b/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts @@ -0,0 +1,89 @@ +/* + * 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 { + ExceptionListItemSchema, + CreateExceptionListSchema, + CreateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; +import { Response } from 'superagent'; +import { FtrService } from '../../functional/ftr_provider_context'; +import { ExceptionsListItemGenerator } from '../../../plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator'; +import { TRUSTED_APPS_EXCEPTION_LIST_DEFINITION } from '../../../plugins/security_solution/public/management/pages/trusted_apps/constants'; +import { EndpointError } from '../../../plugins/security_solution/common/endpoint/errors'; + +export interface ArtifactTestData { + artifact: ExceptionListItemSchema; + cleanup: () => Promise; +} + +export class EndpointArtifactsTestResources extends FtrService { + private readonly exceptionsGenerator = new ExceptionsListItemGenerator(); + private readonly supertest = this.ctx.getService('supertest'); + private readonly log = this.ctx.getService('log'); + + private getHttpResponseFailureHandler( + ignoredStatusCodes: number[] = [] + ): (res: Response) => Promise { + return async (res) => { + if (!res.ok && !ignoredStatusCodes.includes(res.status)) { + throw new EndpointError(JSON.stringify(res.error, null, 2)); + } + + return res; + }; + } + + private async ensureListExists(listDefinition: CreateExceptionListSchema): Promise { + // attempt to create it and ignore 409 (already exists) errors + await this.supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(listDefinition) + .then(this.getHttpResponseFailureHandler([409])); + } + + private async createExceptionItem( + createPayload: CreateExceptionListItemSchema + ): Promise { + const artifact = await this.supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(createPayload) + .then(this.getHttpResponseFailureHandler()) + .then((response) => response.body as ExceptionListItemSchema); + + const { item_id: itemId, namespace_type: namespaceType } = artifact; + + this.log.info(`Created exception list item: ${itemId}`); + + const cleanup = async () => { + const deleteResponse = await this.supertest + .delete(`${EXCEPTION_LIST_ITEM_URL}?item_id=${itemId}&namespace_type=${namespaceType}`) + .set('kbn-xsrf', 'true') + .send() + .then(this.getHttpResponseFailureHandler([404])); + + this.log.info(`Deleted exception list item: ${itemId} (${deleteResponse.status})`); + }; + + return { + artifact, + cleanup, + }; + } + + async createTrustedApp( + overrides: Partial = {} + ): Promise { + await this.ensureListExists(TRUSTED_APPS_EXCEPTION_LIST_DEFINITION); + const trustedApp = this.exceptionsGenerator.generateTrustedAppForCreate(overrides); + + return this.createExceptionItem(trustedApp); + } +} diff --git a/x-pack/test/security_solution_endpoint/services/index.ts b/x-pack/test/security_solution_endpoint/services/index.ts index a7e8985541191..6880ce54e99aa 100644 --- a/x-pack/test/security_solution_endpoint/services/index.ts +++ b/x-pack/test/security_solution_endpoint/services/index.ts @@ -10,10 +10,12 @@ import { EndpointPolicyTestResourcesProvider } from './endpoint_policy'; import { IngestManagerProvider } from '../../common/services/ingest_manager'; import { EndpointTelemetryTestResourcesProvider } from './endpoint_telemetry'; import { EndpointTestResources } from './endpoint'; +import { EndpointArtifactsTestResources } from './endpoint_artifacts'; export const services = { ...xPackFunctionalServices, endpointTestResources: EndpointTestResources, + endpointArtifactTestResources: EndpointArtifactsTestResources, policyTestResources: EndpointPolicyTestResourcesProvider, telemetryTestResources: EndpointTelemetryTestResourcesProvider, ingestManager: IngestManagerProvider, diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts new file mode 100644 index 0000000000000..58f6fbf58ccb5 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts @@ -0,0 +1,229 @@ +/* + * 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 { EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; +import { PolicyTestResourceInfo } from '../../security_solution_endpoint/services/endpoint_policy'; +import { ArtifactTestData } from '../../security_solution_endpoint/services/endpoint_artifacts'; +import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../plugins/security_solution/common/endpoint/service/artifacts'; +import { ExceptionsListItemGenerator } from '../../../plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator'; +import { + createUserAndRole, + deleteUserAndRole, + ROLES, +} from '../../common/services/security_solution'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const endpointPolicyTestResources = getService('endpointPolicyTestResources'); + const endpointArtifactTestResources = getService('endpointArtifactTestResources'); + + describe('Endpoint artifacts (via lists plugin)', () => { + let fleetEndpointPolicy: PolicyTestResourceInfo; + + before(async () => { + // Create an endpoint policy in fleet we can work with + fleetEndpointPolicy = await endpointPolicyTestResources.createPolicy(); + + // create role/user + await createUserAndRole(getService, ROLES.detections_admin); + }); + + after(async () => { + if (fleetEndpointPolicy) { + await fleetEndpointPolicy.cleanup(); + } + + // delete role/user + await deleteUserAndRole(getService, ROLES.detections_admin); + }); + + const anEndpointArtifactError = (res: { body: { message: string } }) => { + expect(res.body.message).to.match(/EndpointArtifactError/); + }; + const anErrorMessageWith = ( + value: string | RegExp + ): ((res: { body: { message: string } }) => void) => { + return (res) => { + if (value instanceof RegExp) { + expect(res.body.message).to.match(value); + } else { + expect(res.body.message).to.be(value); + } + }; + }; + + describe('and accessing trusted apps', () => { + const exceptionsGenerator = new ExceptionsListItemGenerator(); + let trustedAppData: ArtifactTestData; + + type TrustedAppApiCallsInterface = Array<{ + method: keyof Pick; + path: string; + // The body just needs to have the properties we care about in the tests. This should cover most + // mocks used for testing that support different interfaces + getBody: () => Pick; + }>; + + beforeEach(async () => { + trustedAppData = await endpointArtifactTestResources.createTrustedApp({ + tags: [`${BY_POLICY_ARTIFACT_TAG_PREFIX}${fleetEndpointPolicy.packagePolicy.id}`], + }); + }); + + afterEach(async () => { + if (trustedAppData) { + await trustedAppData.cleanup(); + } + }); + + const trustedAppApiCalls: TrustedAppApiCallsInterface = [ + { + method: 'post', + path: EXCEPTION_LIST_ITEM_URL, + getBody: () => exceptionsGenerator.generateTrustedAppForCreate(), + }, + { + method: 'put', + path: EXCEPTION_LIST_ITEM_URL, + getBody: () => + exceptionsGenerator.generateTrustedAppForUpdate({ + id: trustedAppData.artifact.id, + item_id: trustedAppData.artifact.item_id, + }), + }, + ]; + + describe('and has authorization to manage endpoint security', () => { + for (const trustedAppApiCall of trustedAppApiCalls) { + it(`should error on [${trustedAppApiCall.method}] if invalid condition entry fields are used`, async () => { + const body = trustedAppApiCall.getBody(); + + body.entries[0].field = 'some.invalid.field'; + + await supertest[trustedAppApiCall.method](trustedAppApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(400) + .expect(anEndpointArtifactError) + .expect(anErrorMessageWith(/types that failed validation:/)); + }); + + it(`should error on [${trustedAppApiCall.method}] if a condition entry field is used more than once`, async () => { + const body = trustedAppApiCall.getBody(); + + body.entries.push({ ...body.entries[0] }); + + await supertest[trustedAppApiCall.method](trustedAppApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(400) + .expect(anEndpointArtifactError) + .expect(anErrorMessageWith(/Duplicate/)); + }); + + it(`should error on [${trustedAppApiCall.method}] if an invalid hash is used`, async () => { + const body = trustedAppApiCall.getBody(); + + body.entries = [ + { + field: 'process.hash.md5', + operator: 'included', + type: 'match', + value: '1', + }, + ]; + + await supertest[trustedAppApiCall.method](trustedAppApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(400) + .expect(anEndpointArtifactError) + .expect(anErrorMessageWith(/invalid hash/)); + }); + + it(`should error on [${trustedAppApiCall.method}] if signer is set for a non windows os entry item`, async () => { + const body = trustedAppApiCall.getBody(); + + body.os_types = ['linux']; + body.entries = [ + { + field: 'process.Ext.code_signature', + entries: [ + { + field: 'trusted', + value: 'true', + type: 'match', + operator: 'included', + }, + { + field: 'subject_name', + value: 'foo', + type: 'match', + operator: 'included', + }, + ], + type: 'nested', + }, + ]; + + await supertest[trustedAppApiCall.method](trustedAppApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(400) + .expect(anEndpointArtifactError) + .expect(anErrorMessageWith(/^.*(?!process\.Ext\.code_signature)/)); + }); + + it(`should error on [${trustedAppApiCall.method}] if more than one OS is set`, async () => { + const body = trustedAppApiCall.getBody(); + + body.os_types = ['linux', 'windows']; + + await supertest[trustedAppApiCall.method](trustedAppApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(400) + .expect(anEndpointArtifactError) + .expect(anErrorMessageWith(/\[osTypes\]: array size is \[2\]/)); + }); + + it(`should error on [${trustedAppApiCall.method}] if policy id is invalid`, async () => { + const body = trustedAppApiCall.getBody(); + + body.tags = [`${BY_POLICY_ARTIFACT_TAG_PREFIX}123`]; + + await supertest[trustedAppApiCall.method](trustedAppApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(400) + .expect(anEndpointArtifactError) + .expect(anErrorMessageWith(/invalid policy ids/)); + }); + } + }); + + describe('and user DOES NOT have authorization to manage endpoint security', () => { + for (const trustedAppApiCall of trustedAppApiCalls) { + it(`should error on [${trustedAppApiCall.method}]`, async () => { + await supertestWithoutAuth[trustedAppApiCall.method](trustedAppApiCall.path) + .auth(ROLES.detections_admin, 'changeme') + .set('kbn-xsrf', 'true') + .send(trustedAppApiCall.getBody()) + .expect(403, { + status_code: 403, + message: 'EndpointArtifactError: Endpoint authorization failure', + }); + }); + } + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index db2de64b3bbe4..5c2a3f168248c 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -32,5 +32,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./package')); loadTestFile(require.resolve('./endpoint_authz')); + loadTestFile(require.resolve('./endpoint_artifacts')); }); } diff --git a/x-pack/test/security_solution_endpoint_api_int/services/index.ts b/x-pack/test/security_solution_endpoint_api_int/services/index.ts index e4f96333bac31..eb66d738af1b0 100644 --- a/x-pack/test/security_solution_endpoint_api_int/services/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/services/index.ts @@ -8,9 +8,13 @@ import { services as xPackAPIServices } from '../../api_integration/services'; import { ResolverGeneratorProvider } from './resolver'; import { EndpointTestResources } from '../../security_solution_endpoint/services/endpoint'; +import { EndpointPolicyTestResourcesProvider } from '../../security_solution_endpoint/services/endpoint_policy'; +import { EndpointArtifactsTestResources } from '../../security_solution_endpoint/services/endpoint_artifacts'; export const services = { ...xPackAPIServices, resolverGenerator: ResolverGeneratorProvider, endpointTestResources: EndpointTestResources, + endpointPolicyTestResources: EndpointPolicyTestResourcesProvider, + endpointArtifactTestResources: EndpointArtifactsTestResources, }; From 43569cb6850ce23a593830b2db9d4d03972d19ab Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 20 Jan 2022 12:20:40 -0500 Subject: [PATCH 09/43] [Security Solution][Endpoint] Change endpoint authz service to again check for `superuser` role in addition to `fleet.all` (#123425) * Change endpoint authz to use `user.roles` in determining user's authorization * Removed hook that was manually checking for isolation authz --- .../common/endpoint/actions.ts | 14 ------- .../endpoint/service/authz/authz.test.ts | 27 ++++++++++--- .../common/endpoint/service/authz/authz.ts | 7 +++- .../endpoint/use_endpoint_privileges.ts | 24 +++++++++-- .../hooks/endpoint/use_isolate_privileges.ts | 40 ------------------- .../use_host_isolation_action.tsx | 4 +- .../take_action_dropdown/index.test.tsx | 5 +-- .../server/request_context_factory.ts | 3 +- 8 files changed, 52 insertions(+), 72 deletions(-) delete mode 100644 x-pack/plugins/security_solution/common/endpoint/actions.ts delete mode 100644 x-pack/plugins/security_solution/public/common/hooks/endpoint/use_isolate_privileges.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/actions.ts b/x-pack/plugins/security_solution/common/endpoint/actions.ts deleted file mode 100644 index 287ebddacad9a..0000000000000 --- a/x-pack/plugins/security_solution/common/endpoint/actions.ts +++ /dev/null @@ -1,14 +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 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'); -}; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts index 947615bdf7e88..3d64bd98b758e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts @@ -13,10 +13,12 @@ import { EndpointAuthzKeyList } from '../../types/authz'; describe('Endpoint Authz service', () => { let licenseService: ReturnType; let fleetAuthz: FleetAuthz; + let userRoles: string[]; beforeEach(() => { licenseService = createLicenseServiceMock(); fleetAuthz = createFleetAuthzMock(); + userRoles = ['superuser']; }); describe('calculateEndpointAuthz()', () => { @@ -27,24 +29,33 @@ describe('Endpoint Authz service', () => { ['canIsolateHost'], ['canUnIsolateHost'], ])('should set `%s` to `true`', (authProperty) => { - expect(calculateEndpointAuthz(licenseService, fleetAuthz)[authProperty]).toBe(true); + expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles)[authProperty]).toBe( + true + ); }); it('should set `canIsolateHost` to false if not proper license', () => { licenseService.isPlatinumPlus.mockReturnValue(false); - expect(calculateEndpointAuthz(licenseService, fleetAuthz).canIsolateHost).toBe(false); + expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canIsolateHost).toBe( + false + ); }); it('should set `canUnIsolateHost` to true even if not proper license', () => { licenseService.isPlatinumPlus.mockReturnValue(false); - expect(calculateEndpointAuthz(licenseService, fleetAuthz).canUnIsolateHost).toBe(true); + expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canUnIsolateHost).toBe( + true + ); }); }); describe('and `fleet.all` access is false', () => { - beforeEach(() => (fleetAuthz.fleet.all = false)); + beforeEach(() => { + fleetAuthz.fleet.all = false; + userRoles = []; + }); it.each([ ['canAccessFleet'], @@ -52,13 +63,17 @@ describe('Endpoint Authz service', () => { ['canIsolateHost'], ['canUnIsolateHost'], ])('should set `%s` to `false`', (authProperty) => { - expect(calculateEndpointAuthz(licenseService, fleetAuthz)[authProperty]).toBe(false); + expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles)[authProperty]).toBe( + false + ); }); it('should set `canUnIsolateHost` to false when policy is also not platinum', () => { licenseService.isPlatinumPlus.mockReturnValue(false); - expect(calculateEndpointAuthz(licenseService, fleetAuthz).canUnIsolateHost).toBe(false); + expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canUnIsolateHost).toBe( + false + ); }); }); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts index 83bd267e56c53..6b20a376bbf61 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts @@ -8,6 +8,7 @@ import { LicenseService } from '../../../license'; import { FleetAuthz } from '../../../../../fleet/common'; import { EndpointAuthz } from '../../types/authz'; +import { MaybeImmutable } from '../../types'; /** * Used by both the server and the UI to generate the Authorization for access to Endpoint related @@ -15,13 +16,15 @@ import { EndpointAuthz } from '../../types/authz'; * * @param licenseService * @param fleetAuthz + * @param userRoles */ export const calculateEndpointAuthz = ( licenseService: LicenseService, - fleetAuthz: FleetAuthz + fleetAuthz: FleetAuthz, + userRoles: MaybeImmutable ): EndpointAuthz => { const isPlatinumPlusLicense = licenseService.isPlatinumPlus(); - const hasAllAccessToFleet = fleetAuthz.fleet.all; + const hasAllAccessToFleet = fleetAuthz.fleet.all || userRoles.includes('superuser'); return { canAccessFleet: hasAllAccessToFleet, diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts index 6fa0c51f500da..e6c7b9a5d0e95 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts @@ -8,7 +8,11 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { useCurrentUser, useKibana } from '../../../lib/kibana'; import { useLicense } from '../../../hooks/use_license'; -import { EndpointPrivileges, Immutable } from '../../../../../common/endpoint/types'; +import { + EndpointPrivileges, + Immutable, + MaybeImmutable, +} from '../../../../../common/endpoint/types'; import { calculateEndpointAuthz, getEndpointAuthzInitialState, @@ -28,17 +32,19 @@ export const useEndpointPrivileges = (): Immutable => { const licenseService = useLicense(); const [fleetCheckDone, setFleetCheckDone] = useState(false); const [fleetAuthz, setFleetAuthz] = useState(null); + const [userRolesCheckDone, setUserRolesCheckDone] = useState(false); + const [userRoles, setUserRoles] = useState>([]); const privileges = useMemo(() => { const privilegeList: EndpointPrivileges = Object.freeze({ - loading: !fleetCheckDone || !user, + loading: !fleetCheckDone || !userRolesCheckDone || !user, ...(fleetAuthz - ? calculateEndpointAuthz(licenseService, fleetAuthz) + ? calculateEndpointAuthz(licenseService, fleetAuthz, userRoles) : getEndpointAuthzInitialState()), }); return privilegeList; - }, [fleetCheckDone, user, fleetAuthz, licenseService]); + }, [fleetCheckDone, userRolesCheckDone, user, fleetAuthz, licenseService, userRoles]); // Check if user can access fleet useEffect(() => { @@ -64,6 +70,16 @@ export const useEndpointPrivileges = (): Immutable => { })(); }, [fleetServices]); + // get user roles + useEffect(() => { + (async () => { + if (user && isMounted.current) { + setUserRoles(user?.roles); + setUserRolesCheckDone(true); + } + })(); + }, [user]); + // Capture if component is unmounted useEffect( () => () => { diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_isolate_privileges.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_isolate_privileges.ts deleted file mode 100644 index 23ef6d586adc5..0000000000000 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_isolate_privileges.ts +++ /dev/null @@ -1,40 +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 { useEffect, useState } from 'react'; -import { userCanIsolate } from '../../../../common/endpoint/actions'; -import { useKibana } from '../../lib/kibana'; -import { useLicense } from '../use_license'; - -interface IsolationPriviledgesStatus { - isLoading: boolean; - isAllowed: boolean; -} - -/* - * Host isolation requires superuser privileges and at least a platinum license - */ -export const useIsolationPrivileges = (): IsolationPriviledgesStatus => { - const [isLoading, setIsLoading] = useState(false); - const [canIsolate, setCanIsolate] = useState(false); - - const isPlatinumPlus = useLicense().isPlatinumPlus(); - const services = useKibana().services; - - useEffect(() => { - setIsLoading(true); - const user = services.security.authc.getCurrentUser(); - if (user) { - user.then((authenticatedUser) => { - setCanIsolate(userCanIsolate(authenticatedUser.roles)); - setIsLoading(false); - }); - } - }, [services.security.authc]); - - return { isLoading, isAllowed: canIsolate && isPlatinumPlus ? true : false }; -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx index a7c3cb900d7c9..57d81d48fca05 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx @@ -9,11 +9,11 @@ import { EuiContextMenuItem } from '@elastic/eui'; import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { isIsolationSupported } from '../../../../common/endpoint/service/host_isolation/utils'; import { HostStatus } from '../../../../common/endpoint/types'; -import { useIsolationPrivileges } from '../../../common/hooks/endpoint/use_isolate_privileges'; import { isAlertFromEndpointEvent } from '../../../common/utils/endpoint_alert_check'; import { useHostIsolationStatus } from '../../containers/detection_engine/alerts/use_host_isolation_status'; import { ISOLATE_HOST, UNISOLATE_HOST } from './translations'; import { getFieldValue } from './helpers'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; interface UseHostIsolationActionProps { closePopover: () => void; @@ -62,7 +62,7 @@ export const useHostIsolationAction = ({ capabilities, }); - const { isAllowed: isIsolationAllowed } = useIsolationPrivileges(); + const isIsolationAllowed = useUserPrivileges().endpointPrivileges.canIsolateHost; const isolateHostHandler = useCallback(() => { closePopover(); diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx index e12f01458da4c..0c525a2d77706 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx @@ -21,9 +21,6 @@ import { useKibana } from '../../../common/lib/kibana'; jest.mock('../user_info', () => ({ useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]), })); -jest.mock('../../../common/hooks/endpoint/use_isolate_privileges', () => ({ - useIsolationPrivileges: jest.fn().mockReturnValue({ isAllowed: true }), -})); jest.mock('../../../common/lib/kibana', () => ({ useKibana: jest.fn(), useGetUserCasesPermissions: jest.fn().mockReturnValue({ crud: true }), @@ -60,6 +57,8 @@ jest.mock('../../containers/detection_engine/alerts/use_host_isolation_status', }; }); +jest.mock('../../../common/components/user_privileges'); + describe('take action dropdown', () => { const defaultProps: TakeActionDropdownProps = { detailsData: mockAlertDetailsData as TimelineEventsDetailsItem[], diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index c84f2e9eab9ed..26bb56113e635 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -83,7 +83,8 @@ export class RequestContextFactory implements IRequestContextFactory { if (!startPlugins.fleet) { endpointAuthz = getEndpointAuthzInitialState(); } else { - endpointAuthz = calculateEndpointAuthz(licenseService, fleetAuthz); + const userRoles = security?.authc.getCurrentUser(request)?.roles ?? []; + endpointAuthz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles); } } From ff51bb7fef26d99a4fedad9db77ae8045098913c Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 20 Jan 2022 11:25:28 -0600 Subject: [PATCH 10/43] Prevent Negative boosts in Relevance Tuning (#123468) --- .../boosts/boost_item_content/boost_item_content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx index 5abf3652d6622..44c5f276cd20c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx @@ -55,7 +55,7 @@ export const BoostItemContent: React.FC = ({ boost, index, name }) => { fullWidth > Date: Thu, 20 Jan 2022 13:07:46 -0500 Subject: [PATCH 11/43] [Maps][ML] Integration part 1: Create anomalies layer in maps (#122862) * wip: create anomalies layer in maps * ensure anomaly search respects time filter * add custom color ramp and connected layer * update ml api service types * add tooltip * update naming and types * update warning messages. make actual layer default * remove any --- x-pack/plugins/maps/common/index.ts | 1 + .../plugins/ml/common/constants/anomalies.ts | 19 + x-pack/plugins/ml/common/index.ts | 7 +- x-pack/plugins/ml/common/util/job_utils.ts | 6 + x-pack/plugins/ml/public/application/app.tsx | 2 +- .../explorer/explorer_charts/map_config.ts | 20 +- .../services/ml_api_service/jobs.ts | 7 + .../ml/public/maps/anomaly_job_selector.tsx | 86 +++++ .../ml/public/maps/anomaly_layer_wizard.tsx | 30 ++ .../maps/anomaly_layer_wizard_factory.tsx | 101 +++++ .../plugins/ml/public/maps/anomaly_source.tsx | 360 ++++++++++++++++++ .../ml/public/maps/anomaly_source_factory.ts | 42 ++ .../ml/public/maps/anomaly_source_field.ts | 169 ++++++++ .../maps/create_anomaly_source_editor.tsx | 89 +++++ .../plugins/ml/public/maps/layer_selector.tsx | 68 ++++ .../ml/public/maps/register_map_extension.ts | 31 ++ .../maps/update_anomaly_source_editor.tsx | 50 +++ x-pack/plugins/ml/public/maps/util.ts | 153 ++++++++ x-pack/plugins/ml/public/plugin.ts | 19 +- .../ml/public/register_helper/index.ts | 1 + .../ml/server/models/job_service/jobs.ts | 7 + .../plugins/ml/server/routes/job_service.ts | 36 ++ 22 files changed, 1280 insertions(+), 24 deletions(-) create mode 100644 x-pack/plugins/ml/public/maps/anomaly_job_selector.tsx create mode 100644 x-pack/plugins/ml/public/maps/anomaly_layer_wizard.tsx create mode 100644 x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx create mode 100644 x-pack/plugins/ml/public/maps/anomaly_source.tsx create mode 100644 x-pack/plugins/ml/public/maps/anomaly_source_factory.ts create mode 100644 x-pack/plugins/ml/public/maps/anomaly_source_field.ts create mode 100644 x-pack/plugins/ml/public/maps/create_anomaly_source_editor.tsx create mode 100644 x-pack/plugins/ml/public/maps/layer_selector.tsx create mode 100644 x-pack/plugins/ml/public/maps/register_map_extension.ts create mode 100644 x-pack/plugins/ml/public/maps/update_anomaly_source_editor.tsx create mode 100644 x-pack/plugins/ml/public/maps/util.ts diff --git a/x-pack/plugins/maps/common/index.ts b/x-pack/plugins/maps/common/index.ts index f3b8595efc8d1..3e26cdc82fbbb 100644 --- a/x-pack/plugins/maps/common/index.ts +++ b/x-pack/plugins/maps/common/index.ts @@ -33,4 +33,5 @@ export type { TooltipFeature, VectorLayerDescriptor, VectorStyleDescriptor, + VectorSourceRequestMeta, } from './descriptor_types'; diff --git a/x-pack/plugins/ml/common/constants/anomalies.ts b/x-pack/plugins/ml/common/constants/anomalies.ts index 5cca321482a00..4decdcb2b2d28 100644 --- a/x-pack/plugins/ml/common/constants/anomalies.ts +++ b/x-pack/plugins/ml/common/constants/anomalies.ts @@ -31,6 +31,25 @@ export const SEVERITY_COLORS = { BLANK: '#ffffff', }; +export const SEVERITY_COLOR_RAMP = [ + { + stop: ANOMALY_THRESHOLD.LOW, + color: SEVERITY_COLORS.WARNING, + }, + { + stop: ANOMALY_THRESHOLD.MINOR, + color: SEVERITY_COLORS.MINOR, + }, + { + stop: ANOMALY_THRESHOLD.MAJOR, + color: SEVERITY_COLORS.MAJOR, + }, + { + stop: ANOMALY_THRESHOLD.CRITICAL, + color: SEVERITY_COLORS.CRITICAL, + }, +]; + export const ANOMALY_RESULT_TYPE = { BUCKET: 'bucket', RECORD: 'record', diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index c76b662df7a5a..de5989d92d208 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -7,7 +7,12 @@ export { ES_CLIENT_TOTAL_HITS_RELATION } from './types/es_client'; export type { ChartData } from './types/field_histograms'; -export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD, SEVERITY_COLORS } from './constants/anomalies'; +export { + ANOMALY_SEVERITY, + ANOMALY_THRESHOLD, + SEVERITY_COLOR_RAMP, + SEVERITY_COLORS, +} from './constants/anomalies'; export { getSeverityColor, getSeverityType } from './util/anomaly_utils'; export { isPopulatedObject } from './util/object_utils'; export { composeValidators, patternValidator } from './util/validators'; diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index e66d8de5bd15e..f1991118d4e36 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -73,6 +73,12 @@ export function isMappableJob(job: CombinedJob, detectorIndex: number): boolean return isMappable; } +// Returns a boolean indicating whether the specified job is suitable for maps plugin. +export function isJobWithGeoData(job: Job): boolean { + const { detectors } = job.analysis_config; + return detectors.some((detector) => detector.function === ML_JOB_AGGREGATION.LAT_LONG); +} + /** * Validates that composite definition only have sources that are only terms and date_histogram * if composite is defined. diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index e61549efe8adc..d38a37716b82c 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -28,7 +28,7 @@ import { mlApiServicesProvider } from './services/ml_api_service'; import { HttpService } from './services/http_service'; import { ML_APP_LOCATOR, ML_PAGES } from '../../common/constants/locator'; -export type MlDependencies = Omit & +export type MlDependencies = Omit & MlStartDependencies; interface AppProps { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts index 7f3378fc1c858..51b9732aebdd0 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts @@ -6,29 +6,11 @@ */ import { FIELD_ORIGIN, LAYER_TYPE, STYLE_TYPE } from '../../../../../maps/common'; -import { ANOMALY_THRESHOLD, SEVERITY_COLORS } from '../../../../common'; +import { SEVERITY_COLOR_RAMP } from '../../../../common'; import { AnomaliesTableData } from '../explorer_utils'; const FEATURE = 'Feature'; const POINT = 'Point'; -const SEVERITY_COLOR_RAMP = [ - { - stop: ANOMALY_THRESHOLD.LOW, - color: SEVERITY_COLORS.WARNING, - }, - { - stop: ANOMALY_THRESHOLD.MINOR, - color: SEVERITY_COLORS.MINOR, - }, - { - stop: ANOMALY_THRESHOLD.MAJOR, - color: SEVERITY_COLORS.MAJOR, - }, - { - stop: ANOMALY_THRESHOLD.CRITICAL, - color: SEVERITY_COLORS.CRITICAL, - }, -]; function getAnomalyFeatures( anomalies: AnomaliesTableData['anomalies'], diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index 96c5e1abce170..f1492074ed762 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -45,6 +45,13 @@ export const jobsApiProvider = (httpService: HttpService) => ({ }); }, + jobIdsWithGeo() { + return httpService.http({ + path: `${ML_BASE_PATH}/jobs/jobs_with_geo`, + method: 'GET', + }); + }, + jobsWithTimerange(dateFormatTz: string) { const body = JSON.stringify({ dateFormatTz }); return httpService.http<{ diff --git a/x-pack/plugins/ml/public/maps/anomaly_job_selector.tsx b/x-pack/plugins/ml/public/maps/anomaly_job_selector.tsx new file mode 100644 index 0000000000000..aacc8e37d935b --- /dev/null +++ b/x-pack/plugins/ml/public/maps/anomaly_job_selector.tsx @@ -0,0 +1,86 @@ +/* + * 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 React, { Component } from 'react'; + +import { EuiComboBox, EuiFormRow, EuiComboBoxOptionOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEqual } from 'lodash'; +import type { MlApiServices } from '../application/services/ml_api_service'; + +interface Props { + onJobChange: (jobId: string) => void; + mlJobsService: MlApiServices['jobs']; +} + +interface State { + jobId?: string; + jobIdList?: Array>; +} + +export class AnomalyJobSelector extends Component { + private _isMounted: boolean = false; + + state: State = {}; + + private async _loadJobs() { + const jobIdList = await this.props.mlJobsService.jobIdsWithGeo(); + const options = jobIdList.map((jobId) => { + return { label: jobId, value: jobId }; + }); + if (this._isMounted && !isEqual(options, this.state.jobIdList)) { + this.setState({ + jobIdList: options, + }); + } + } + + componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { + this._loadJobs(); + } + + componentDidMount(): void { + this._isMounted = true; + this._loadJobs(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + onJobIdSelect = (selectedOptions: Array>) => { + const jobId: string = selectedOptions[0].value!; + if (this._isMounted) { + this.setState({ jobId }); + this.props.onJobChange(jobId); + } + }; + + render() { + if (!this.state.jobIdList) { + return null; + } + + return ( + + + + ); + } +} diff --git a/x-pack/plugins/ml/public/maps/anomaly_layer_wizard.tsx b/x-pack/plugins/ml/public/maps/anomaly_layer_wizard.tsx new file mode 100644 index 0000000000000..b45cce35a89ba --- /dev/null +++ b/x-pack/plugins/ml/public/maps/anomaly_layer_wizard.tsx @@ -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 { i18n } from '@kbn/i18n'; +import { LAYER_WIZARD_CATEGORY } from '../../../maps/common'; +import type { LayerWizard } from '../../../maps/public'; + +export const anomalyLayerWizard: Partial = { + categories: [LAYER_WIZARD_CATEGORY.SOLUTIONS, LAYER_WIZARD_CATEGORY.ELASTICSEARCH], + description: i18n.translate('xpack.ml.maps.anomalyLayerDescription', { + defaultMessage: 'Display anomalies from a machine learning job', + }), + disabledReason: i18n.translate('xpack.ml.maps.anomalyLayerUnavailableMessage', { + defaultMessage: + 'Anomalies layers are a subscription feature. Ensure you have the right subscription and access to Machine Learning.', + }), + icon: 'outlierDetectionJob', + getIsDisabled: () => { + // return false by default + return false; + }, + title: i18n.translate('xpack.ml.maps.anomalyLayerTitle', { + defaultMessage: 'ML Anomalies', + }), + order: 100, +}; diff --git a/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx b/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx new file mode 100644 index 0000000000000..70a81c89816a2 --- /dev/null +++ b/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx @@ -0,0 +1,101 @@ +/* + * 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 React from 'react'; +import { htmlIdGenerator } from '@elastic/eui'; +import type { StartServicesAccessor } from 'kibana/public'; +import type { LayerWizard, RenderWizardArguments } from '../../../maps/public'; +import { FIELD_ORIGIN, LAYER_TYPE, STYLE_TYPE } from '../../../maps/common'; +import { SEVERITY_COLOR_RAMP } from '../../common'; +import { CreateAnomalySourceEditor } from './create_anomaly_source_editor'; +import { + VectorLayerDescriptor, + VectorStylePropertiesDescriptor, +} from '../../../maps/common/descriptor_types'; +import { AnomalySource, AnomalySourceDescriptor } from './anomaly_source'; + +import { HttpService } from '../application/services/http_service'; +import type { MlPluginStart, MlStartDependencies } from '../plugin'; +import type { MlApiServices } from '../application/services/ml_api_service'; + +export const ML_ANOMALY = 'ML_ANOMALIES'; +const CUSTOM_COLOR_RAMP = { + type: STYLE_TYPE.DYNAMIC, + options: { + customColorRamp: SEVERITY_COLOR_RAMP, + field: { + name: 'record_score', + origin: FIELD_ORIGIN.SOURCE, + }, + useCustomColorRamp: true, + }, +}; + +export class AnomalyLayerWizardFactory { + public readonly type = ML_ANOMALY; + + constructor( + private getStartServices: StartServicesAccessor, + private canGetJobs: boolean + ) { + this.canGetJobs = canGetJobs; + } + + private async getServices(): Promise<{ mlJobsService: MlApiServices['jobs'] }> { + const [coreStart] = await this.getStartServices(); + const { jobsApiProvider } = await import('../application/services/ml_api_service/jobs'); + + const httpService = new HttpService(coreStart.http); + const mlJobsService = jobsApiProvider(httpService); + + return { mlJobsService }; + } + + public async create(): Promise { + const { mlJobsService } = await this.getServices(); + const { anomalyLayerWizard } = await import('./anomaly_layer_wizard'); + + anomalyLayerWizard.getIsDisabled = () => !this.canGetJobs; + + anomalyLayerWizard.renderWizard = ({ previewLayers }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: Partial | null) => { + if (!sourceConfig) { + previewLayers([]); + return; + } + + const anomalyLayerDescriptor: VectorLayerDescriptor = { + id: htmlIdGenerator()(), + type: LAYER_TYPE.GEOJSON_VECTOR, + sourceDescriptor: AnomalySource.createDescriptor({ + jobId: sourceConfig.jobId, + typicalActual: sourceConfig.typicalActual, + }), + style: { + type: 'VECTOR', + properties: { + fillColor: CUSTOM_COLOR_RAMP, + lineColor: CUSTOM_COLOR_RAMP, + } as unknown as VectorStylePropertiesDescriptor, + isTimeAware: false, + }, + }; + + previewLayers([anomalyLayerDescriptor]); + }; + + return ( + + ); + }; + + return anomalyLayerWizard as LayerWizard; + } +} diff --git a/x-pack/plugins/ml/public/maps/anomaly_source.tsx b/x-pack/plugins/ml/public/maps/anomaly_source.tsx new file mode 100644 index 0000000000000..1159f97dcbec9 --- /dev/null +++ b/x-pack/plugins/ml/public/maps/anomaly_source.tsx @@ -0,0 +1,360 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { ReactElement } from 'react'; +import { + FieldFormatter, + MAX_ZOOM, + MIN_ZOOM, + VECTOR_SHAPE_TYPE, + VectorSourceRequestMeta, +} from '../../../maps/common'; +import { AbstractSourceDescriptor, MapExtent } from '../../../maps/common/descriptor_types'; +import { ITooltipProperty } from '../../../maps/public'; +import { + AnomalySourceField, + AnomalySourceTooltipProperty, + ANOMALY_SOURCE_FIELDS, +} from './anomaly_source_field'; +import type { Adapters } from '../../../../../src/plugins/inspector/common/adapters'; +import type { GeoJsonWithMeta } from '../../../maps/public'; +import type { IField } from '../../../maps/public'; +import type { Attribution, ImmutableSourceProperty, PreIndexedShape } from '../../../maps/public'; +import type { SourceEditorArgs } from '../../../maps/public'; +import type { DataRequest } from '../../../maps/public'; +import type { IVectorSource, SourceStatus } from '../../../maps/public'; +import { ML_ANOMALY } from './anomaly_source_factory'; +import { getResultsForJobId, MlAnomalyLayers } from './util'; +import { UpdateAnomalySourceEditor } from './update_anomaly_source_editor'; +import type { MlApiServices } from '../application/services/ml_api_service'; + +export interface AnomalySourceDescriptor extends AbstractSourceDescriptor { + jobId: string; + typicalActual: MlAnomalyLayers; +} + +export class AnomalySource implements IVectorSource { + static mlResultsService: MlApiServices['results']; + static canGetJobs: boolean; + + static createDescriptor(descriptor: Partial) { + if (typeof descriptor.jobId !== 'string') { + throw new Error('Job id is required for anomaly layer creation'); + } + + return { + type: ML_ANOMALY, + jobId: descriptor.jobId, + typicalActual: descriptor.typicalActual || 'actual', + }; + } + + private readonly _descriptor: AnomalySourceDescriptor; + + constructor(sourceDescriptor: Partial, adapters?: Adapters) { + this._descriptor = AnomalySource.createDescriptor(sourceDescriptor); + } + // TODO: implement query awareness + async getGeoJsonWithMeta( + layerName: string, + searchFilters: VectorSourceRequestMeta, + registerCancelCallback: (callback: () => void) => void, + isRequestStillActive: () => boolean + ): Promise { + const results = await getResultsForJobId( + AnomalySource.mlResultsService, + this._descriptor.jobId, + this._descriptor.typicalActual, + searchFilters + ); + + return { + data: results, + meta: { + // Set this to true if data is incomplete (e.g. capping number of results to first 1k) + areResultsTrimmed: false, + }, + }; + } + + canFormatFeatureProperties(): boolean { + return false; + } + + cloneDescriptor(): AnomalySourceDescriptor { + return { + type: this._descriptor.type, + jobId: this._descriptor.jobId, + typicalActual: this._descriptor.typicalActual, + }; + } + + createField({ fieldName }: { fieldName: string }): IField { + if (fieldName !== 'record_score') { + throw new Error('Record score field name is required'); + } + return new AnomalySourceField({ source: this, field: fieldName }); + } + + async createFieldFormatter(field: IField): Promise { + return null; + } + + destroy(): void {} + + getApplyGlobalQuery(): boolean { + return false; + } + + getApplyForceRefresh(): boolean { + return false; + } + + getApplyGlobalTime(): boolean { + return true; + } + + async getAttributions(): Promise { + return []; + } + + async getBoundsForFilters( + boundsFilters: object, + registerCancelCallback: (callback: () => void) => void + ): Promise { + return null; + } + + async getDisplayName(): Promise { + return i18n.translate('xpack.ml.maps.anomalySource.displayLabel', { + defaultMessage: '{typicalActual} for {jobId}', + values: { + typicalActual: this._descriptor.typicalActual, + jobId: this._descriptor.jobId, + }, + }); + } + + getFieldByName(fieldName: string): IField | null { + if (fieldName === 'record_score') { + return new AnomalySourceField({ source: this, field: fieldName }); + } + return null; + } + + getSourceStatus() { + return { tooltipContent: null, areResultsTrimmed: true }; + } + + getType(): string { + return this._descriptor.type; + } + + isMvt() { + return true; + } + + showJoinEditor(): boolean { + // Ignore, only show if joins are enabled for current configuration + return false; + } + + getFieldNames(): string[] { + return Object.keys(ANOMALY_SOURCE_FIELDS); + } + + async getFields(): Promise { + return this.getFieldNames().map((field) => new AnomalySourceField({ source: this, field })); + } + + getGeoGridPrecision(zoom: number): number { + return 0; + } + + isBoundsAware(): boolean { + return false; + } + + async getImmutableProperties(): Promise { + return [ + { + label: i18n.translate('xpack.ml.maps.anomalySourcePropLabel', { + defaultMessage: 'Job Id', + }), + value: this._descriptor.jobId, + }, + ]; + } + + async isTimeAware(): Promise { + return true; + } + + renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement | null { + return ( + + ); + } + + async supportsFitToBounds(): Promise { + // Return true if you can compute bounds of data + return true; + } + + async getLicensedFeatures(): Promise<[]> { + return []; + } + + getMaxZoom(): number { + return MAX_ZOOM; + } + + getMinZoom(): number { + return MIN_ZOOM; + } + + getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceStatus { + return { + tooltipContent: i18n.translate('xpack.ml.maps.sourceTooltip', { + defaultMessage: 'Shows anomalies', + }), + // set to true if data is incomplete (we limit to first 1000 results) + areResultsTrimmed: true, + }; + } + + async getSupportedShapeTypes(): Promise { + return this._descriptor.typicalActual === 'connected' + ? [VECTOR_SHAPE_TYPE.LINE] + : [VECTOR_SHAPE_TYPE.POINT]; + } + + getSyncMeta(): object | null { + return { + jobId: this._descriptor.jobId, + typicalActual: this._descriptor.typicalActual, + }; + } + + async getTooltipProperties(properties: { [p: string]: any } | null): Promise { + const tooltipProperties: ITooltipProperty[] = []; + for (const key in properties) { + if (properties.hasOwnProperty(key)) { + const label = ANOMALY_SOURCE_FIELDS[key]?.label; + if (label) { + tooltipProperties.push(new AnomalySourceTooltipProperty(label, properties[key])); + } + } + } + return tooltipProperties; + } + + isFieldAware(): boolean { + return true; + } + + // This is for type-ahead support in the UX for by-value styling + async getValueSuggestions(field: IField, query: string): Promise { + return []; + } + + // ----------------- + // API ML probably can ignore + getAttributionProvider() { + return null; + } + + getIndexPatternIds(): string[] { + // IGNORE: This is only relevant if your source is backed by an index-pattern + return []; + } + + getInspectorAdapters(): Adapters | undefined { + // IGNORE: This is only relevant if your source is backed by an index-pattern + return undefined; + } + + getJoinsDisabledReason(): string | null { + // IGNORE: This is only relevant if your source can be joined to other data + return null; + } + + async getLeftJoinFields(): Promise { + // IGNORE: This is only relevant if your source can be joined to other data + return []; + } + + async getPreIndexedShape( + properties: { [p: string]: any } | null + ): Promise { + // IGNORE: This is only relevant if your source is backed by an index-pattern + return null; + } + + getQueryableIndexPatternIds(): string[] { + // IGNORE: This is only relevant if your source is backed by an index-pattern + return []; + } + + isESSource(): boolean { + // IGNORE: This is only relevant if your source is backed by an index-pattern + return false; + } + + isFilterByMapBounds(): boolean { + // Only implement if you can query this data with a bounding-box + return false; + } + + isGeoGridPrecisionAware(): boolean { + // Ignore: only implement if your data is scale-dependent (probably not) + return false; + } + + isQueryAware(): boolean { + // IGNORE: This is only relevant if your source is backed by an index-pattern + return false; + } + + isRefreshTimerAware(): boolean { + // Allow force-refresh when user clicks "refresh" button in the global time-picker + return true; + } + + async getTimesliceMaskFieldName() { + return null; + } + + async supportsFeatureEditing() { + return false; + } + + hasTooltipProperties() { + return true; + } + + async addFeature() { + // should not be called + } + + async deleteFeature() { + // should not be called + } + + getUpdateDueToTimeslice() { + // TODO + return true; + } + + async getDefaultFields(): Promise>> { + return {}; + } +} diff --git a/x-pack/plugins/ml/public/maps/anomaly_source_factory.ts b/x-pack/plugins/ml/public/maps/anomaly_source_factory.ts new file mode 100644 index 0000000000000..71aefbfd0fdb9 --- /dev/null +++ b/x-pack/plugins/ml/public/maps/anomaly_source_factory.ts @@ -0,0 +1,42 @@ +/* + * 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 { StartServicesAccessor } from 'kibana/public'; +import { HttpService } from '../application/services/http_service'; +import type { MlPluginStart, MlStartDependencies } from '../plugin'; +import type { MlApiServices } from '../application/services/ml_api_service'; + +export const ML_ANOMALY = 'ML_ANOMALIES'; + +export class AnomalySourceFactory { + public readonly type = ML_ANOMALY; + + constructor( + private getStartServices: StartServicesAccessor, + private canGetJobs: boolean + ) { + this.canGetJobs = canGetJobs; + } + + private async getServices(): Promise<{ mlResultsService: MlApiServices['results'] }> { + const [coreStart] = await this.getStartServices(); + const { mlApiServicesProvider } = await import('../application/services/ml_api_service'); + + const httpService = new HttpService(coreStart.http); + const mlResultsService = mlApiServicesProvider(httpService).results; + + return { mlResultsService }; + } + + public async create(): Promise { + const { mlResultsService } = await this.getServices(); + const { AnomalySource } = await import('./anomaly_source'); + AnomalySource.mlResultsService = mlResultsService; + AnomalySource.canGetJobs = this.canGetJobs; + return AnomalySource; + } +} diff --git a/x-pack/plugins/ml/public/maps/anomaly_source_field.ts b/x-pack/plugins/ml/public/maps/anomaly_source_field.ts new file mode 100644 index 0000000000000..8a0e1f0104ee0 --- /dev/null +++ b/x-pack/plugins/ml/public/maps/anomaly_source_field.ts @@ -0,0 +1,169 @@ +/* + * 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-next-line max-classes-per-file +import { escape } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { IField, IVectorSource } from '../../../maps/public'; +import { FIELD_ORIGIN } from '../../../maps/common'; +import { TileMetaFeature } from '../../../maps/common/descriptor_types'; +import { AnomalySource } from './anomaly_source'; +import { ITooltipProperty } from '../../../maps/public'; +import { Filter } from '../../../../../src/plugins/data/public'; + +export const ANOMALY_SOURCE_FIELDS: Record> = { + record_score: { + label: i18n.translate('xpack.ml.maps.anomalyLayerRecordScoreLabel', { + defaultMessage: 'Record score', + }), + type: 'number', + }, + timestamp: { + label: i18n.translate('xpack.ml.maps.anomalyLayerTimeStampLabel', { + defaultMessage: 'Time', + }), + type: 'string', + }, + fieldName: { + label: i18n.translate('xpack.ml.maps.anomalyLayerFieldNameLabel', { + defaultMessage: 'Field name', + }), + type: 'string', + }, + functionDescription: { + label: i18n.translate('xpack.ml.maps.anomalyLayerFunctionDescriptionLabel', { + defaultMessage: 'Function', + }), + type: 'string', + }, + actualDisplay: { + label: i18n.translate('xpack.ml.maps.anomalyLayerActualLabel', { + defaultMessage: 'Actual', + }), + type: 'string', + }, + typicalDisplay: { + label: i18n.translate('xpack.ml.maps.anomalyLayerTypicalLabel', { + defaultMessage: 'Typical', + }), + type: 'string', + }, +}; + +export class AnomalySourceTooltipProperty implements ITooltipProperty { + constructor(private readonly _label: string, private readonly _value: string) {} + + async getESFilters(): Promise { + return []; + } + + getHtmlDisplayValue(): string { + return this._value.toString(); + } + + getPropertyKey(): string { + return this._label; + } + + getPropertyName(): string { + return this._label; + } + + getRawValue(): string | string[] | undefined { + return this._value.toString(); + } + + isFilterable(): boolean { + return false; + } +} + +// this needs to be generic so it works for all fields in anomaly record result +export class AnomalySourceField implements IField { + private readonly _source: AnomalySource; + private readonly _field: string; + + constructor({ source, field }: { source: AnomalySource; field: string }) { + this._source = source; + this._field = field; + } + + async createTooltipProperty(value: string | string[] | undefined): Promise { + return new AnomalySourceTooltipProperty( + await this.getLabel(), + escape(Array.isArray(value) ? value.join() : value ? value : '') + ); + } + + async getDataType(): Promise { + return ANOMALY_SOURCE_FIELDS[this._field].type; + } + + async getLabel(): Promise { + return ANOMALY_SOURCE_FIELDS[this._field].label; + } + + getName(): string { + return this._field; + } + + getMbFieldName(): string { + return this.getName(); + } + + getOrigin(): FIELD_ORIGIN { + return FIELD_ORIGIN.SOURCE; + } + + getRootName(): string { + return this.getName(); + } + + getSource(): IVectorSource { + return this._source; + } + + isEqual(field: IField): boolean { + return this.getName() === field.getName(); + } + + isValid(): boolean { + return true; + } + + supportsFieldMetaFromLocalData(): boolean { + return true; + } + + supportsFieldMetaFromEs(): boolean { + return false; + } + + canValueBeFormatted(): boolean { + return false; + } + + async getExtendedStatsFieldMetaRequest(): Promise { + return null; + } + + async getPercentilesFieldMetaRequest(percentiles: number[]): Promise { + return null; + } + + async getCategoricalFieldMetaRequest(size: number): Promise { + return null; + } + + pluckRangeFromTileMetaFeature(metaFeature: TileMetaFeature): { min: number; max: number } | null { + return null; + } + + isCount(): boolean { + return false; + } +} diff --git a/x-pack/plugins/ml/public/maps/create_anomaly_source_editor.tsx b/x-pack/plugins/ml/public/maps/create_anomaly_source_editor.tsx new file mode 100644 index 0000000000000..e04fd40f9416b --- /dev/null +++ b/x-pack/plugins/ml/public/maps/create_anomaly_source_editor.tsx @@ -0,0 +1,89 @@ +/* + * 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 React, { Component } from 'react'; + +import { EuiPanel } from '@elastic/eui'; +import { AnomalySourceDescriptor } from './anomaly_source'; +import { AnomalyJobSelector } from './anomaly_job_selector'; +import { LayerSelector } from './layer_selector'; +import { MlAnomalyLayers } from './util'; +import type { MlApiServices } from '../application/services/ml_api_service'; + +interface Props { + onSourceConfigChange: (sourceConfig: Partial | null) => void; + mlJobsService: MlApiServices['jobs']; +} + +interface State { + jobId?: string; + typicalActual?: MlAnomalyLayers; +} + +export class CreateAnomalySourceEditor extends Component { + private _isMounted: boolean = false; + state: State = {}; + + private configChange() { + if (this.state.jobId) { + this.props.onSourceConfigChange({ + jobId: this.state.jobId, + typicalActual: this.state.typicalActual, + }); + } + } + + componentDidMount(): void { + this._isMounted = true; + } + + private onTypicalActualChange = (typicalActual: MlAnomalyLayers) => { + if (!this._isMounted) { + return; + } + this.setState( + { + typicalActual, + }, + () => { + this.configChange(); + } + ); + }; + + private previewLayer = (jobId: string) => { + if (!this._isMounted) { + return; + } + this.setState( + { + jobId, + }, + () => { + this.configChange(); + } + ); + }; + + render() { + const selector = this.state.jobId ? ( + + ) : null; + return ( + + + {selector} + + ); + } +} diff --git a/x-pack/plugins/ml/public/maps/layer_selector.tsx b/x-pack/plugins/ml/public/maps/layer_selector.tsx new file mode 100644 index 0000000000000..2998187ad465b --- /dev/null +++ b/x-pack/plugins/ml/public/maps/layer_selector.tsx @@ -0,0 +1,68 @@ +/* + * 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 React, { Component } from 'react'; + +import { EuiComboBox, EuiFormRow, EuiComboBoxOptionOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { MlAnomalyLayers } from './util'; + +interface Props { + onChange: (typicalActual: MlAnomalyLayers) => void; + typicalActual: MlAnomalyLayers; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface State {} + +export class LayerSelector extends Component { + private _isMounted: boolean = false; + + state: State = {}; + + componentDidMount(): void { + this._isMounted = true; + } + + componentWillUnmount() { + this._isMounted = false; + } + + onSelect = (selectedOptions: Array>) => { + const typicalActual: MlAnomalyLayers = selectedOptions[0].value! as + | 'typical' + | 'actual' + | 'connected'; + if (this._isMounted) { + this.setState({ typicalActual }); + this.props.onChange(typicalActual); + } + }; + + render() { + const options = [{ value: this.props.typicalActual, label: this.props.typicalActual }]; + return ( + + + + ); + } +} diff --git a/x-pack/plugins/ml/public/maps/register_map_extension.ts b/x-pack/plugins/ml/public/maps/register_map_extension.ts new file mode 100644 index 0000000000000..6cd8bd1f2a3e8 --- /dev/null +++ b/x-pack/plugins/ml/public/maps/register_map_extension.ts @@ -0,0 +1,31 @@ +/* + * 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 { MapsSetupApi } from '../../../maps/public'; +import type { MlCoreSetup } from '../plugin'; +import { AnomalySourceFactory } from './anomaly_source_factory'; +import { AnomalyLayerWizardFactory } from './anomaly_layer_wizard_factory'; + +export async function registerMapExtension( + mapsSetupApi: MapsSetupApi, + core: MlCoreSetup, + canGetJobs: boolean +) { + const anomalySourceFactory = new AnomalySourceFactory(core.getStartServices, canGetJobs); + const anomalyLayerWizardFactory = new AnomalyLayerWizardFactory( + core.getStartServices, + canGetJobs + ); + const anomalylayerWizard = await anomalyLayerWizardFactory.create(); + + mapsSetupApi.registerSource({ + type: anomalySourceFactory.type, + ConstructorFunction: await anomalySourceFactory.create(), + }); + + mapsSetupApi.registerLayerWizard(anomalylayerWizard); +} diff --git a/x-pack/plugins/ml/public/maps/update_anomaly_source_editor.tsx b/x-pack/plugins/ml/public/maps/update_anomaly_source_editor.tsx new file mode 100644 index 0000000000000..7e53e6ebf9f5d --- /dev/null +++ b/x-pack/plugins/ml/public/maps/update_anomaly_source_editor.tsx @@ -0,0 +1,50 @@ +/* + * 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 React, { Fragment, Component } from 'react'; + +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { LayerSelector } from './layer_selector'; +import { MlAnomalyLayers } from './util'; + +interface Props { + onChange: (...args: Array<{ propName: string; value: unknown }>) => void; + typicalActual: MlAnomalyLayers; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface State {} + +export class UpdateAnomalySourceEditor extends Component { + state: State = {}; + + render() { + return ( + + + +
+ +
+
+ + { + this.props.onChange({ + propName: 'typicalActual', + value: typicalActual, + }); + }} + typicalActual={this.props.typicalActual} + /> +
+ +
+ ); + } +} diff --git a/x-pack/plugins/ml/public/maps/util.ts b/x-pack/plugins/ml/public/maps/util.ts new file mode 100644 index 0000000000000..6a9d55ad64d38 --- /dev/null +++ b/x-pack/plugins/ml/public/maps/util.ts @@ -0,0 +1,153 @@ +/* + * 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 { FeatureCollection, Feature, Geometry } from 'geojson'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { ESSearchResponse } from '../../../../../src/core/types/elasticsearch'; +import { formatHumanReadableDateTimeSeconds } from '../../common/util/date_utils'; +import type { MlApiServices } from '../application/services/ml_api_service'; +import { MLAnomalyDoc } from '../../common/types/anomalies'; +import { VectorSourceRequestMeta } from '../../../maps/common'; + +export type MlAnomalyLayers = 'typical' | 'actual' | 'connected'; + +// Must reverse coordinates here. Map expects [lon, lat] - anomalies are stored as [lat, lon] for lat_lon jobs +function getCoordinates(actualCoordinateStr: string, round: boolean = false): number[] { + const convertWithRounding = (point: string) => Math.round(Number(point) * 100) / 100; + const convert = (point: string) => Number(point); + return actualCoordinateStr + .split(',') + .map(round ? convertWithRounding : convert) + .reverse(); +} + +export async function getResultsForJobId( + mlResultsService: MlApiServices['results'], + jobId: string, + locationType: MlAnomalyLayers, + searchFilters: VectorSourceRequestMeta +): Promise { + const { timeFilters } = searchFilters; + + const must: estypes.QueryDslQueryContainer[] = [ + { term: { job_id: jobId } }, + { term: { result_type: 'record' } }, + ]; + + // Query to look for the highest scoring anomaly. + const body: estypes.SearchRequest['body'] = { + query: { + bool: { + must, + }, + }, + size: 1000, + _source: { + excludes: [], + }, + }; + + if (timeFilters) { + const timerange = { + range: { + timestamp: { + gte: `${timeFilters.from}`, + lte: timeFilters.to, + }, + }, + }; + must.push(timerange); + } + + let resp: ESSearchResponse | null = null; + let hits: Array<{ + actual: number[]; + actualDisplay: number[]; + fieldName?: string; + functionDescription: string; + typical: number[]; + typicalDisplay: number[]; + record_score: number; + timestamp: string; + }> = []; + + try { + resp = await mlResultsService.anomalySearch( + { + body, + }, + [jobId] + ); + } catch (error) { + // search may fail if the job doesn't already exist + // ignore this error as the outer function call will raise a toast + } + if (resp !== null && resp.hits.total.value > 0) { + hits = resp.hits.hits.map(({ _source }) => { + const geoResults = _source.geo_results; + const actualCoordStr = geoResults && geoResults.actual_point; + const typicalCoordStr = geoResults && geoResults.typical_point; + let typical: number[] = []; + let typicalDisplay: number[] = []; + let actual: number[] = []; + let actualDisplay: number[] = []; + + if (actualCoordStr !== undefined) { + actual = getCoordinates(actualCoordStr); + actualDisplay = getCoordinates(actualCoordStr, true); + } + if (typicalCoordStr !== undefined) { + typical = getCoordinates(typicalCoordStr); + typicalDisplay = getCoordinates(typicalCoordStr, true); + } + return { + fieldName: _source.field_name, + functionDescription: _source.function_description, + timestamp: formatHumanReadableDateTimeSeconds(_source.timestamp), + typical, + typicalDisplay, + actual, + actualDisplay, + record_score: Math.floor(_source.record_score), + }; + }); + } + + const features: Feature[] = hits.map((result) => { + let geometry: Geometry; + if (locationType === 'typical' || locationType === 'actual') { + geometry = { + type: 'Point', + coordinates: locationType === 'typical' ? result.typical : result.actual, + }; + } else { + geometry = { + type: 'LineString', + coordinates: [result.typical, result.actual], + }; + } + return { + type: 'Feature', + geometry, + properties: { + actual: result.actual, + actualDisplay: result.actualDisplay, + typical: result.typical, + typicalDisplay: result.typicalDisplay, + fieldName: result.fieldName, + functionDescription: result.functionDescription, + timestamp: result.timestamp, + record_score: result.record_score, + }, + }; + }); + + return { + type: 'FeatureCollection', + features, + }; +} diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 2a5cb2ab4ae2a..6563a7bc5b551 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -36,7 +36,7 @@ import { isFullLicense, isMlEnabled } from '../common/license'; import { setDependencyCache } from './application/util/dependency_cache'; import { registerFeature } from './register_feature'; import { MlLocatorDefinition, MlLocator } from './locator'; -import type { MapsStartApi } from '../../maps/public'; +import type { MapsStartApi, MapsSetupApi } from '../../maps/public'; import { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, @@ -66,6 +66,7 @@ export interface MlStartDependencies { export interface MlSetupDependencies { security?: SecurityPluginSetup; + maps?: MapsSetupApi; licensing: LicensingPluginSetup; management?: ManagementSetup; licenseManagement?: LicenseManagementUIPluginSetup; @@ -159,11 +160,23 @@ export class MlPlugin implements Plugin { // register various ML plugin features which require a full license // note including registerFeature in register_helper would cause the page bundle size to increase significantly - const { registerEmbeddables, registerMlUiActions, registerSearchLinks, registerMlAlerts } = - await import('./register_helper'); + const { + registerEmbeddables, + registerMlUiActions, + registerSearchLinks, + registerMlAlerts, + registerMapExtension, + } = await import('./register_helper'); const mlEnabled = isMlEnabled(license); const fullLicense = isFullLicense(license); + + if (pluginsSetup.maps) { + // Pass capabilites.ml.canGetJobs as minimum permission to show anomalies card in maps layers + const canGetJobs = capabilities.ml?.canGetJobs === true || false; + await registerMapExtension(pluginsSetup.maps, core, canGetJobs); + } + if (mlEnabled) { registerSearchLinks(this.appUpdater$, fullLicense); diff --git a/x-pack/plugins/ml/public/register_helper/index.ts b/x-pack/plugins/ml/public/register_helper/index.ts index 278f32f683053..47d9bad31997a 100644 --- a/x-pack/plugins/ml/public/register_helper/index.ts +++ b/x-pack/plugins/ml/public/register_helper/index.ts @@ -10,3 +10,4 @@ export { registerManagementSection } from '../application/management'; export { registerMlUiActions } from '../ui_actions'; export { registerSearchLinks } from './register_search_links'; export { registerMlAlerts } from '../alerting'; +export { registerMapExtension } from '../maps/register_map_extension'; diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index a79093caabbef..c4e215fc3c38e 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -11,6 +11,7 @@ import { IScopedClusterClient } from 'kibana/server'; import { getSingleMetricViewerJobErrorMessage, parseTimeIntervalForJob, + isJobWithGeoData, } from '../../../common/util/job_utils'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; import { @@ -272,6 +273,11 @@ export function jobsProvider( return jobs; } + async function getJobIdsWithGeo(): Promise { + const { body } = await mlClient.getJobs(); + return body.jobs.filter(isJobWithGeoData).map((job) => job.job_id); + } + async function jobsWithTimerange() { const fullJobsList = await createFullJobsList(); const jobsMap: { [id: string]: string[] } = {}; @@ -669,5 +675,6 @@ export function jobsProvider( getAllJobAndGroupIds, getLookBackProgress, bulkCreate, + getJobIdsWithGeo, }; } diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 96ca56baa38da..123b38e9cd7de 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -285,6 +285,42 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { }) ); + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs_with_geo Jobs summary + * @apiName JobsSummary + * @apiDescription Returns a list of anomaly detection jobs with analysis config with fields supported by maps. + * + * @apiSuccess {Array} jobIds list of job ids. + */ + router.get( + { + path: '/api/ml/jobs/jobs_with_geo', + validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, response, context }) => { + try { + const { getJobIdsWithGeo } = jobServiceProvider( + client, + mlClient, + context.alerting?.getRulesClient() + ); + + const resp = await getJobIdsWithGeo(); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + /** * @apiGroup JobService * From d49880b807e9d8e3ab46265cb1abf7f02fc93250 Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Thu, 20 Jan 2022 19:09:33 +0100 Subject: [PATCH 12/43] fixes advanced seetings default message (#123480) --- .../public/common/components/news_feed/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/common/components/news_feed/translations.ts b/x-pack/plugins/security_solution/public/common/components/news_feed/translations.ts index 6b4c342b57b29..e21f6165f3763 100644 --- a/x-pack/plugins/security_solution/public/common/components/news_feed/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/news_feed/translations.ts @@ -22,6 +22,6 @@ export const NO_NEWS_MESSAGE_ADMIN = i18n.translate( export const ADVANCED_SETTINGS_LINK_TITLE = i18n.translate( 'xpack.securitySolution.newsFeed.advancedSettingsLinkTitle', { - defaultMessage: 'SIEM advanced settings', + defaultMessage: 'Security Solution advanced settings', } ); From d744500c2466aaecfad4364749b6d4a5c33af373 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 20 Jan 2022 19:12:44 +0100 Subject: [PATCH 13/43] [ML] Remove export wildcard syntax for file upload plugin. (#123205) Remove export wildcard syntax for file upload plugin. --- .../plugins/data_visualizer/common/constants.ts | 7 +++++++ .../common/components/combined_fields/utils.ts | 2 +- .../application/common/components/utils/utils.ts | 8 ++------ .../analysis_summary/analysis_summary.tsx | 3 ++- .../components/edit_flyout/options/options.ts | 2 +- .../components/edit_flyout/overrides.js | 2 +- .../components/edit_flyout/overrides.test.js | 2 +- x-pack/plugins/file_upload/common/constants.ts | 2 -- x-pack/plugins/file_upload/common/index.ts | 16 +++++++++++----- x-pack/plugins/file_upload/common/types.ts | 4 ++-- x-pack/plugins/file_upload/public/api/index.ts | 2 +- .../geojson_upload_form/geojson_file_picker.tsx | 2 +- .../public/components/json_upload_and_parse.tsx | 2 +- .../geojson_importer/geojson_importer.ts | 3 ++- .../file_upload/public/importer/get_max_bytes.ts | 6 +++--- .../file_upload/public/importer/importer.ts | 6 +++--- .../public/importer/message_importer.ts | 8 ++++---- .../plugins/file_upload/public/importer/types.ts | 4 ++-- x-pack/plugins/file_upload/public/index.ts | 5 ----- .../plugins/file_upload/server/analyze_file.tsx | 7 ++++++- x-pack/plugins/file_upload/server/import_data.ts | 2 +- x-pack/plugins/file_upload/server/plugin.ts | 2 +- x-pack/plugins/file_upload/server/routes.ts | 9 ++------- .../ml/common/constants/data_frame_analytics.ts | 5 +++++ .../data_frame_analytics/analytics_manager.ts | 6 +++--- 25 files changed, 63 insertions(+), 54 deletions(-) diff --git a/x-pack/plugins/data_visualizer/common/constants.ts b/x-pack/plugins/data_visualizer/common/constants.ts index cc661ca6ffeff..ab25ecff4045d 100644 --- a/x-pack/plugins/data_visualizer/common/constants.ts +++ b/x-pack/plugins/data_visualizer/common/constants.ts @@ -21,6 +21,13 @@ export const FILE_SIZE_DISPLAY_FORMAT = '0,0.[0] b'; // index as having been created by the File Data Visualizer. export const INDEX_META_DATA_CREATED_BY = 'file-data-visualizer'; +export const FILE_FORMATS = { + DELIMITED: 'delimited', + NDJSON: 'ndjson', + SEMI_STRUCTURED_TEXT: 'semi_structured_text', + // XML: 'xml', +}; + export const JOB_FIELD_TYPES = { BOOLEAN: 'boolean', DATE: 'date', diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/combined_fields/utils.ts b/x-pack/plugins/data_visualizer/public/application/common/components/combined_fields/utils.ts index e021de5e5beca..84f67c3222885 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/combined_fields/utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/combined_fields/utils.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { cloneDeep } from 'lodash'; import uuid from 'uuid/v4'; import { CombinedField } from './types'; -import { +import type { FindFileStructureResponse, IngestPipeline, Mappings, diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/utils/utils.ts b/x-pack/plugins/data_visualizer/public/application/common/components/utils/utils.ts index 3dd889976eb11..5f9317be6d7c5 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/utils/utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/utils/utils.ts @@ -6,12 +6,8 @@ */ import { isEqual } from 'lodash'; -import { - AnalysisResult, - InputOverrides, - MB, - FILE_FORMATS, -} from '../../../../../../file_upload/common'; +import type { AnalysisResult, InputOverrides } from '../../../../../../file_upload/common'; +import { MB, FILE_FORMATS } from '../../../../../common'; export const DEFAULT_LINES_TO_SAMPLE = 1000; const UPLOAD_SIZE_MB = 5; diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/analysis_summary/analysis_summary.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/analysis_summary/analysis_summary.tsx index b00266e5d3580..b754bfd4691ec 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/analysis_summary/analysis_summary.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/analysis_summary/analysis_summary.tsx @@ -9,7 +9,8 @@ import { FormattedMessage } from '@kbn/i18n-react'; import React, { FC } from 'react'; import { EuiTitle, EuiSpacer, EuiDescriptionList } from '@elastic/eui'; -import { FindFileStructureResponse, FILE_FORMATS } from '../../../../../../file_upload/common'; +import type { FindFileStructureResponse } from '../../../../../../file_upload/common'; +import { FILE_FORMATS } from '../../../../../common'; export const AnalysisSummary: FC<{ results: FindFileStructureResponse }> = ({ results }) => { const items = createDisplayItems(results); diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/options/options.ts b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/options/options.ts index 066778705724d..e0c3e7f2d58cc 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/options/options.ts +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/options/options.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FILE_FORMATS } from '../../../../../../../file_upload/common'; +import { FILE_FORMATS } from '../../../../../../common/'; import { TIMESTAMP_OPTIONS, diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.js b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.js index 84e207dd5b01e..d0213813c8f16 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.js +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.js @@ -23,7 +23,7 @@ import { EuiTextArea, } from '@elastic/eui'; -import { FILE_FORMATS } from '../../../../../../file_upload/common'; +import { FILE_FORMATS } from '../../../../../common'; import { getFormatOptions, diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.test.js b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.test.js index aac12c386f346..8e9b0d0806fc1 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.test.js +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.test.js @@ -7,7 +7,7 @@ import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { FILE_FORMATS } from '../../../../../../file_upload/common/constants'; +import { FILE_FORMATS } from '../../../../../common'; import { Overrides } from './overrides'; diff --git a/x-pack/plugins/file_upload/common/constants.ts b/x-pack/plugins/file_upload/common/constants.ts index ce1c13e18c803..725cd3f81a650 100644 --- a/x-pack/plugins/file_upload/common/constants.ts +++ b/x-pack/plugins/file_upload/common/constants.ts @@ -6,11 +6,9 @@ */ export const UI_SETTING_MAX_FILE_SIZE = 'fileUpload:maxFileSize'; - export const MB = Math.pow(2, 20); export const MAX_FILE_SIZE = '100MB'; export const MAX_FILE_SIZE_BYTES = 104857600; // 100MB - export const ABSOLUTE_MAX_FILE_SIZE_BYTES = 1073741274; // 1GB export const FILE_SIZE_DISPLAY_FORMAT = '0,0.[0] b'; diff --git a/x-pack/plugins/file_upload/common/index.ts b/x-pack/plugins/file_upload/common/index.ts index 58159dfc3d7ef..5d312835c3e06 100644 --- a/x-pack/plugins/file_upload/common/index.ts +++ b/x-pack/plugins/file_upload/common/index.ts @@ -5,8 +5,14 @@ * 2.0. */ -// TODO: https://github.com/elastic/kibana/issues/110898 -/* eslint-disable @kbn/eslint/no_export_all */ - -export * from './constants'; -export * from './types'; +/** + * @internal + */ +export type { + AnalysisResult, + FindFileStructureErrorResponse, + FindFileStructureResponse, + InputOverrides, + IngestPipeline, + Mappings, +} from './types'; diff --git a/x-pack/plugins/file_upload/common/types.ts b/x-pack/plugins/file_upload/common/types.ts index 6e72b749bdb61..5a4ec4a3872ad 100644 --- a/x-pack/plugins/file_upload/common/types.ts +++ b/x-pack/plugins/file_upload/common/types.ts @@ -118,11 +118,11 @@ export interface ImportFailure { doc: ImportDoc; } -export interface Doc { +export interface ImportDocMessage { message: string; } -export type ImportDoc = Doc | string | object; +export type ImportDoc = ImportDocMessage | string | object; export interface Settings { pipeline?: string; diff --git a/x-pack/plugins/file_upload/public/api/index.ts b/x-pack/plugins/file_upload/public/api/index.ts index f3a184f27dfac..d34cb2099120e 100644 --- a/x-pack/plugins/file_upload/public/api/index.ts +++ b/x-pack/plugins/file_upload/public/api/index.ts @@ -7,7 +7,7 @@ import { lazyLoadModules } from '../lazy_load_bundle'; import type { IImporter, ImportFactoryOptions } from '../importer'; -import type { HasImportPermission, FindFileStructureResponse } from '../../common'; +import type { HasImportPermission, FindFileStructureResponse } from '../../common/types'; import type { getMaxBytes, getMaxBytesFormatted } from '../importer/get_max_bytes'; import { JsonUploadAndParseAsyncWrapper } from './json_upload_and_parse_async_wrapper'; import { IndexNameFormAsyncWrapper } from './index_name_form_async_wrapper'; diff --git a/x-pack/plugins/file_upload/public/components/geojson_upload_form/geojson_file_picker.tsx b/x-pack/plugins/file_upload/public/components/geojson_upload_form/geojson_file_picker.tsx index 6cd55e3a0a74a..20c3189a8f346 100644 --- a/x-pack/plugins/file_upload/public/components/geojson_upload_form/geojson_file_picker.tsx +++ b/x-pack/plugins/file_upload/public/components/geojson_upload_form/geojson_file_picker.tsx @@ -8,7 +8,7 @@ import React, { Component } from 'react'; import { EuiFilePicker, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { MB } from '../../../common'; +import { MB } from '../../../common/constants'; import { getMaxBytesFormatted } from '../../importer/get_max_bytes'; import { validateFile } from '../../importer'; import { diff --git a/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx index 80ff5910ac1ef..5b786d91e25aa 100644 --- a/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx +++ b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx @@ -15,7 +15,7 @@ import { ES_FIELD_TYPES } from '../../../../../src/plugins/data/public'; import type { FileUploadComponentProps, FileUploadGeoResults } from '../lazy_load_bundle'; import { ImportResults } from '../importer'; import { GeoJsonImporter } from '../importer/geojson_importer'; -import { Settings } from '../../common'; +import type { Settings } from '../../common/types'; import { hasImportPermission } from '../api'; enum PHASE { diff --git a/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.ts b/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.ts index 24150051771a1..40d6abdb1603a 100644 --- a/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.ts +++ b/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.ts @@ -14,7 +14,8 @@ import { callImportRoute, Importer, IMPORT_RETRIES, MAX_CHUNK_CHAR_COUNT } from import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; // @ts-expect-error import { geoJsonCleanAndValidate } from './geojson_clean_and_validate'; -import { ImportDoc, ImportFailure, ImportResponse, MB } from '../../../common'; +import { MB } from '../../../common/constants'; +import type { ImportDoc, ImportFailure, ImportResponse } from '../../../common/types'; const BLOCK_SIZE_MB = 5 * MB; export const GEOJSON_FILE_TYPES = ['.json', '.geojson']; diff --git a/x-pack/plugins/file_upload/public/importer/get_max_bytes.ts b/x-pack/plugins/file_upload/public/importer/get_max_bytes.ts index f1ca532692e77..e05f1978dcf95 100644 --- a/x-pack/plugins/file_upload/public/importer/get_max_bytes.ts +++ b/x-pack/plugins/file_upload/public/importer/get_max_bytes.ts @@ -7,12 +7,12 @@ import numeral from '@elastic/numeral'; import { - MAX_FILE_SIZE, - MAX_FILE_SIZE_BYTES, ABSOLUTE_MAX_FILE_SIZE_BYTES, FILE_SIZE_DISPLAY_FORMAT, + MAX_FILE_SIZE, + MAX_FILE_SIZE_BYTES, UI_SETTING_MAX_FILE_SIZE, -} from '../../common'; +} from '../../common/constants'; import { getUiSettings } from '../kibana_services'; export function getMaxBytes() { diff --git a/x-pack/plugins/file_upload/public/importer/importer.ts b/x-pack/plugins/file_upload/public/importer/importer.ts index 103afe4992c71..58809e736720c 100644 --- a/x-pack/plugins/file_upload/public/importer/importer.ts +++ b/x-pack/plugins/file_upload/public/importer/importer.ts @@ -9,15 +9,15 @@ import { chunk, intersection } from 'lodash'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { getHttp } from '../kibana_services'; -import { +import { MB } from '../../common/constants'; +import type { ImportDoc, ImportFailure, ImportResponse, Mappings, Settings, IngestPipeline, - MB, -} from '../../common'; +} from '../../common/types'; import { CreateDocsResponse, IImporter, ImportResults } from './types'; import { isPopulatedObject } from '../../common/utils'; diff --git a/x-pack/plugins/file_upload/public/importer/message_importer.ts b/x-pack/plugins/file_upload/public/importer/message_importer.ts index 21f884d22bc35..1322437f471d0 100644 --- a/x-pack/plugins/file_upload/public/importer/message_importer.ts +++ b/x-pack/plugins/file_upload/public/importer/message_importer.ts @@ -6,7 +6,7 @@ */ import { Importer } from './importer'; -import { Doc } from '../../common'; +import { ImportDocMessage } from '../../common/types'; import { CreateDocsResponse, ImportFactoryOptions } from './types'; export class MessageImporter extends Importer { @@ -33,7 +33,7 @@ export class MessageImporter extends Importer { protected _createDocs(text: string, isLastPart: boolean): CreateDocsResponse { let remainder = 0; try { - const docs: Doc[] = []; + const docs: ImportDocMessage[] = []; let message = ''; let line = ''; @@ -82,7 +82,7 @@ export class MessageImporter extends Importer { } } - private _processLine(data: Doc[], message: string, line: string) { + private _processLine(data: ImportDocMessage[], message: string, line: string) { if (this._excludeLinesRegex === null || line.match(this._excludeLinesRegex) === null) { if (this._multilineStartRegex === null || line.match(this._multilineStartRegex) !== null) { this._addMessage(data, message); @@ -100,7 +100,7 @@ export class MessageImporter extends Importer { return message; } - private _addMessage(data: Doc[], message: string) { + private _addMessage(data: ImportDocMessage[], message: string) { // if the message ended \r\n (Windows line endings) // then omit the \r as well as the \n for consistency message = message.replace(/\r$/, ''); diff --git a/x-pack/plugins/file_upload/public/importer/types.ts b/x-pack/plugins/file_upload/public/importer/types.ts index 7300b7cacfc7f..0ef8ace26f546 100644 --- a/x-pack/plugins/file_upload/public/importer/types.ts +++ b/x-pack/plugins/file_upload/public/importer/types.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { +import type { ImportFailure, IngestPipeline, ImportDoc, ImportResponse, Mappings, Settings, -} from '../../common'; +} from '../../common/types'; export interface ImportConfig { settings: Settings; diff --git a/x-pack/plugins/file_upload/public/index.ts b/x-pack/plugins/file_upload/public/index.ts index 00b39dca1180d..ba591b5c8d942 100644 --- a/x-pack/plugins/file_upload/public/index.ts +++ b/x-pack/plugins/file_upload/public/index.ts @@ -5,17 +5,12 @@ * 2.0. */ -// TODO: https://github.com/elastic/kibana/issues/110898 -/* eslint-disable @kbn/eslint/no_export_all */ - import { FileUploadPlugin } from './plugin'; export function plugin() { return new FileUploadPlugin(); } -export * from './importer/types'; - export type { Props as IndexNameFormProps } from './components/geojson_upload_form/index_name_form'; export type { FileUploadPluginStart } from './plugin'; diff --git a/x-pack/plugins/file_upload/server/analyze_file.tsx b/x-pack/plugins/file_upload/server/analyze_file.tsx index 2239697083492..ecefd34b0cefc 100644 --- a/x-pack/plugins/file_upload/server/analyze_file.tsx +++ b/x-pack/plugins/file_upload/server/analyze_file.tsx @@ -6,7 +6,12 @@ */ import { IScopedClusterClient } from 'kibana/server'; -import { AnalysisResult, FormattedOverrides, InputData, InputOverrides } from '../common'; +import type { + AnalysisResult, + FormattedOverrides, + InputData, + InputOverrides, +} from '../common/types'; export async function analyzeFile( client: IScopedClusterClient, diff --git a/x-pack/plugins/file_upload/server/import_data.ts b/x-pack/plugins/file_upload/server/import_data.ts index 1289adb59925b..2419710c04c5a 100644 --- a/x-pack/plugins/file_upload/server/import_data.ts +++ b/x-pack/plugins/file_upload/server/import_data.ts @@ -14,7 +14,7 @@ import { Settings, Mappings, IngestPipelineWrapper, -} from '../common'; +} from '../common/types'; export function importDataProvider({ asCurrentUser }: IScopedClusterClient) { async function importData( diff --git a/x-pack/plugins/file_upload/server/plugin.ts b/x-pack/plugins/file_upload/server/plugin.ts index bd5eebe372a75..9ec234c415e14 100644 --- a/x-pack/plugins/file_upload/server/plugin.ts +++ b/x-pack/plugins/file_upload/server/plugin.ts @@ -10,7 +10,7 @@ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from ' import { schema } from '@kbn/config-schema'; import { fileUploadRoutes } from './routes'; import { initFileUploadTelemetry } from './telemetry'; -import { UI_SETTING_MAX_FILE_SIZE, MAX_FILE_SIZE } from '../common'; +import { MAX_FILE_SIZE, UI_SETTING_MAX_FILE_SIZE } from '../common/constants'; import { setupCapabilities } from './capabilities'; import { StartDeps, SetupDeps } from './types'; diff --git a/x-pack/plugins/file_upload/server/routes.ts b/x-pack/plugins/file_upload/server/routes.ts index cb7c37a586b64..e8d32152c8afc 100644 --- a/x-pack/plugins/file_upload/server/routes.ts +++ b/x-pack/plugins/file_upload/server/routes.ts @@ -8,13 +8,8 @@ import { schema } from '@kbn/config-schema'; import { IScopedClusterClient } from 'kibana/server'; import { CoreSetup, Logger } from 'src/core/server'; -import { - MAX_FILE_SIZE_BYTES, - IngestPipelineWrapper, - InputData, - Mappings, - Settings, -} from '../common'; +import { MAX_FILE_SIZE_BYTES } from '../common/constants'; +import type { IngestPipelineWrapper, InputData, Mappings, Settings } from '../common/types'; import { wrapError } from './error_wrapper'; import { importDataProvider } from './import_data'; import { getTimeFieldRange } from './get_time_field_range'; diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts index f13537892875d..0a4bc0b165e02 100644 --- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -29,6 +29,11 @@ export const JOB_MAP_NODE_TYPES = { TRAINED_MODEL: 'trainedModel', } as const; +export const INDEX_CREATED_BY = { + FILE_DATA_VISUALIZER: 'file-data-visualizer', + DATA_FRAME_ANALYTICS: 'data-frame-analytics', +} as const; + export const BUILT_IN_MODEL_TAG = 'prepackaged'; export type JobMapNodeTypes = typeof JOB_MAP_NODE_TYPES[keyof typeof JOB_MAP_NODE_TYPES]; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts index dc39a8005b99e..bf0ffc05dd014 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts @@ -8,6 +8,7 @@ import Boom from '@hapi/boom'; import { IScopedClusterClient } from 'kibana/server'; import { + INDEX_CREATED_BY, JOB_MAP_NODE_TYPES, JobMapNodeTypes, } from '../../../common/constants/data_frame_analytics'; @@ -19,7 +20,6 @@ import { DataFrameAnalyticsStats, MapElements, } from '../../../common/types/data_frame_analytics'; -import { INDEX_META_DATA_CREATED_BY } from '../../../../file_upload/common'; import { getAnalysisType } from '../../../common/util/analytics_utils'; import { ExtendAnalyticsMapArgs, @@ -458,14 +458,14 @@ export class AnalyticsManager { if ( link.isWildcardIndexPattern === false && (link.meta === undefined || - link.meta?.created_by.includes(INDEX_META_DATA_CREATED_BY)) + link.meta?.created_by.includes(INDEX_CREATED_BY.FILE_DATA_VISUALIZER)) ) { rootIndexPattern = nextLinkId; complete = true; break; } - if (link.meta?.created_by === 'data-frame-analytics') { + if (link.meta?.created_by === INDEX_CREATED_BY.DATA_FRAME_ANALYTICS) { nextLinkId = link.meta.analytics; nextType = JOB_MAP_NODE_TYPES.ANALYTICS; } From ee34a7a694aa05f9ccc35908a7ce2c8ddb04d9bc Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 20 Jan 2022 12:36:41 -0600 Subject: [PATCH 14/43] [Rule Registry] Remove constant_keyword field mappings from ECS component template (#123486) * Remove constant_keyword field mappings from ECS component template These ECS fields are meant for specific indices (datastreams), but do not make sense (especially as `constant_keyword`) on our alerts-as-data indices, as they will cause errors when attempting to ingest different values from different indices. * Filter out constant_keyword fields from ECS fieldmap The results of this were already applied to the fields themselves, but this ensures that the script does not accidentally repopulate them in the future. Implementation-wise, it was simpler to refactor this into a reduce() rather than explicitly using filter() and then generating a new object. --- .../common/assets/field_maps/ecs_field_map.ts | 15 --------- .../scripts/generate_ecs_fieldmap/index.js | 32 ++++++++++++------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts index 7c4095cca039f..dc81e200032f7 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts @@ -415,21 +415,6 @@ export const ecsFieldMap = { array: false, required: false, }, - 'data_stream.dataset': { - type: 'constant_keyword', - array: false, - required: false, - }, - 'data_stream.namespace': { - type: 'constant_keyword', - array: false, - required: false, - }, - 'data_stream.type': { - type: 'constant_keyword', - array: false, - required: false, - }, 'destination.address': { type: 'keyword', array: false, diff --git a/x-pack/plugins/rule_registry/scripts/generate_ecs_fieldmap/index.js b/x-pack/plugins/rule_registry/scripts/generate_ecs_fieldmap/index.js index 5e90a3c16aa7c..9f28f87b492c5 100644 --- a/x-pack/plugins/rule_registry/scripts/generate_ecs_fieldmap/index.js +++ b/x-pack/plugins/rule_registry/scripts/generate_ecs_fieldmap/index.js @@ -9,7 +9,7 @@ const fs = require('fs'); const util = require('util'); const yaml = require('js-yaml'); const { exec: execCb } = require('child_process'); -const { mapValues } = require('lodash'); +const { reduce } = require('lodash'); const exists = util.promisify(fs.exists); const readFile = util.promisify(fs.readFile); @@ -32,19 +32,27 @@ async function generate() { const flatYaml = await yaml.safeLoad(await readFile(ecsYamlFilename)); - const fields = mapValues(flatYaml, (description) => { - const field = { - type: description.type, - array: description.normalize.includes('array'), - required: !!description.required, - }; + const fields = reduce( + flatYaml, + (fieldsObj, value, key) => { + const field = { + type: value.type, + array: value.normalize.includes('array'), + required: !!value.required, + }; - if (description.scaling_factor) { - field.scaling_factor = description.scaling_factor; - } + if (value.scaling_factor) { + field.scaling_factor = value.scaling_factor; + } - return field; - }); + if (field.type !== 'constant_keyword') { + fieldsObj[key] = field; + } + + return fieldsObj; + }, + {} + ); await Promise.all([ writeFile( From 855bfd30d1766948470fde55613140fc3739be5a Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 20 Jan 2022 14:17:15 -0500 Subject: [PATCH 15/43] Fix endpointAppContextServices use of calculateEndpointAuthz (#123494) --- .../server/endpoint/endpoint_app_context_services.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 3a8616f5d3627..eedc2fac4fb20 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 @@ -139,7 +139,9 @@ export class EndpointAppContextService { public async getEndpointAuthz(request: KibanaRequest): Promise { const fleetAuthz = await this.getFleetAuthzService().fromRequest(request); - return calculateEndpointAuthz(this.getLicenseService(), fleetAuthz); + const userRoles = this.startDependencies?.security.authc.getCurrentUser(request)?.roles ?? []; + + return calculateEndpointAuthz(this.getLicenseService(), fleetAuthz, userRoles); } public getEndpointMetadataService(): EndpointMetadataService { From 77d633fd53585c04e731ff3ff2d3d0103e013b3a Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 20 Jan 2022 20:21:08 +0100 Subject: [PATCH 16/43] [Cases] Fix missing displayName for some react components (#123460) --- x-pack/plugins/cases/public/common/mock/test_providers.tsx | 1 + x-pack/plugins/cases/public/components/all_cases/columns.tsx | 1 + x-pack/plugins/cases/public/components/all_cases/count.tsx | 1 + x-pack/plugins/cases/public/components/all_cases/header.tsx | 1 + x-pack/plugins/cases/public/components/all_cases/index.tsx | 1 + .../plugins/cases/public/components/all_cases/nav_buttons.tsx | 1 + .../plugins/cases/public/components/all_cases/status_filter.tsx | 1 + x-pack/plugins/cases/public/components/all_cases/table.tsx | 1 + .../plugins/cases/public/components/all_cases/utility_bar.tsx | 1 + x-pack/plugins/cases/public/components/app/routes.tsx | 1 + .../plugins/cases/public/components/case_action_bar/actions.tsx | 1 + .../plugins/cases/public/components/case_action_bar/index.tsx | 1 + .../public/components/case_action_bar/status_context_menu.tsx | 1 + x-pack/plugins/cases/public/components/cases_context/index.tsx | 1 + .../cases/public/components/configure_cases/closure_options.tsx | 1 + .../public/components/configure_cases/closure_options_radio.tsx | 1 + .../cases/public/components/configure_cases/connectors.tsx | 1 + .../public/components/configure_cases/connectors_dropdown.tsx | 1 + .../cases/public/components/configure_cases/field_mapping.tsx | 1 + .../components/configure_cases/field_mapping_row_static.tsx | 1 + .../plugins/cases/public/components/configure_cases/mapping.tsx | 1 + .../cases/public/components/confirm_delete_case/index.tsx | 1 + .../plugins/cases/public/components/connector_selector/form.tsx | 1 + x-pack/plugins/cases/public/components/connectors/card.tsx | 1 + .../cases/public/components/connectors/case/alert_fields.tsx | 1 + .../cases/public/components/connectors/case/cases_dropdown.tsx | 1 + .../cases/public/components/connectors/case/existing_case.tsx | 1 + .../cases/public/components/connectors/deprecated_callout.tsx | 1 + .../plugins/cases/public/components/connectors/fields_form.tsx | 1 + .../cases/public/components/connectors/jira/case_fields.tsx | 1 + .../cases/public/components/connectors/jira/search_issues.tsx | 1 + .../cases/public/components/connectors/swimlane/case_fields.tsx | 1 + x-pack/plugins/cases/public/components/create/connector.tsx | 1 + .../plugins/cases/public/components/create/owner_selector.tsx | 1 + x-pack/plugins/cases/public/components/create/submit_button.tsx | 1 + x-pack/plugins/cases/public/components/formatted_date/index.tsx | 1 + .../header_page/__snapshots__/editable_title.test.tsx.snap | 2 +- .../components/header_page/__snapshots__/index.test.tsx.snap | 2 +- .../components/header_page/__snapshots__/title.test.tsx.snap | 2 +- .../cases/public/components/header_page/editable_title.tsx | 1 + x-pack/plugins/cases/public/components/header_page/index.tsx | 1 + x-pack/plugins/cases/public/components/header_page/title.tsx | 1 + .../cases/public/components/markdown_editor/markdown_link.tsx | 1 + .../public/components/markdown_editor/plugins/lens/plugin.tsx | 1 + .../components/markdown_editor/plugins/lens/processor.tsx | 1 + .../cases/public/components/markdown_editor/renderer.tsx | 1 + .../cases/public/components/recent_cases/recent_cases.tsx | 1 + x-pack/plugins/cases/public/components/status/button.tsx | 1 + x-pack/plugins/cases/public/components/status/status.tsx | 1 + x-pack/plugins/cases/public/components/tag_list/tags.tsx | 1 + x-pack/plugins/cases/public/components/truncated_text/index.tsx | 1 + .../components/use_create_case_modal/create_case_modal.tsx | 1 + .../public/components/use_push_to_service/callout/callout.tsx | 1 + .../public/components/use_push_to_service/callout/index.tsx | 1 + x-pack/plugins/cases/public/components/user_actions/avatar.tsx | 1 + .../cases/public/components/user_actions/avatar_username.tsx | 1 + .../public/components/user_actions/comment/alert_event.tsx | 1 + .../components/user_actions/comment/host_isolation_event.tsx | 1 + .../cases/public/components/user_actions/comment/show_alert.tsx | 1 + .../cases/public/components/user_actions/content_toolbar.tsx | 1 + .../plugins/cases/public/components/user_actions/copy_link.tsx | 1 + .../cases/public/components/user_actions/move_to_reference.tsx | 1 + .../cases/public/components/user_actions/property_actions.tsx | 1 + .../plugins/cases/public/components/user_actions/timestamp.tsx | 1 + .../plugins/cases/public/components/user_actions/username.tsx | 1 + 65 files changed, 65 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index b5a3df84c9712..477738ddeac16 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -43,6 +43,7 @@ const TestProvidersComponent: React.FC = ({ ); }; +TestProvidersComponent.displayName = 'TestProviders'; export const TestProviders = React.memo(TestProvidersComponent); diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index 663779029fcec..e67ec11fef03d 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -456,3 +456,4 @@ export const ExternalServiceColumn: React.FC = ({ theCase, connectors })

); }; +ExternalServiceColumn.displayName = 'ExternalServiceColumn'; diff --git a/x-pack/plugins/cases/public/components/all_cases/count.tsx b/x-pack/plugins/cases/public/components/all_cases/count.tsx index 1f6e71c377ee6..cd4abdd08b8e7 100644 --- a/x-pack/plugins/cases/public/components/all_cases/count.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/count.tsx @@ -56,3 +56,4 @@ export const Count: FunctionComponent = ({ refresh }) => { ); }; +Count.displayName = 'Count'; diff --git a/x-pack/plugins/cases/public/components/all_cases/header.tsx b/x-pack/plugins/cases/public/components/all_cases/header.tsx index bca8ea3cf0711..19a1a897221e7 100644 --- a/x-pack/plugins/cases/public/components/all_cases/header.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/header.tsx @@ -64,3 +64,4 @@ export const CasesTableHeader: FunctionComponent = ({ ); +CasesTableHeader.displayName = 'CasesTableHeader'; diff --git a/x-pack/plugins/cases/public/components/all_cases/index.tsx b/x-pack/plugins/cases/public/components/all_cases/index.tsx index e163d9ada4a51..8ea7681eb44d9 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.tsx @@ -33,6 +33,7 @@ export const AllCases: React.FC = () => { ); }; +AllCases.displayName = 'AllCases'; // eslint-disable-next-line import/no-default-export export { AllCases as default }; diff --git a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx index c05210f4e719f..5409cf092d8c9 100644 --- a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx @@ -64,3 +64,4 @@ export const NavButtons: FunctionComponent = ({ actionsErrors }) => { ); }; +NavButtons.displayName = 'NavButtons'; diff --git a/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx index 71359c2e50582..ca211107bcf9a 100644 --- a/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx @@ -50,5 +50,6 @@ const StatusFilterComponent: React.FC = ({ /> ); }; +StatusFilterComponent.displayName = 'StatusFilter'; export const StatusFilter = memo(StatusFilterComponent); diff --git a/x-pack/plugins/cases/public/components/all_cases/table.tsx b/x-pack/plugins/cases/public/components/all_cases/table.tsx index 2a613395119bc..0d4a4cb6b38f1 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx @@ -164,3 +164,4 @@ export const CasesTable: FunctionComponent = ({ ); }; +CasesTable.displayName = 'CasesTable'; diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx index b6ab44517bb66..3ff3f89de210b 100644 --- a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx @@ -161,3 +161,4 @@ export const CasesTableUtilityBar: FunctionComponent = ({ ); }; +CasesTableUtilityBar.displayName = 'CasesTableUtilityBar'; diff --git a/x-pack/plugins/cases/public/components/app/routes.tsx b/x-pack/plugins/cases/public/components/app/routes.tsx index 06387072c2323..ab4bf2ac51f19 100644 --- a/x-pack/plugins/cases/public/components/app/routes.tsx +++ b/x-pack/plugins/cases/public/components/app/routes.tsx @@ -98,5 +98,6 @@ const CasesRoutesComponent: React.FC = ({ ); }; +CasesRoutesComponent.displayName = 'CasesRoutes'; export const CasesRoutes = React.memo(CasesRoutesComponent); diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx index 4cad00535d165..a6db64b83bf09 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx @@ -64,5 +64,6 @@ const ActionsComponent: React.FC = ({ caseData, currentExternal ); }; +ActionsComponent.displayName = 'Actions'; export const Actions = React.memo(ActionsComponent); diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx index 1432d6d707df4..c9e54e87db8d4 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx @@ -164,5 +164,6 @@ const CaseActionBarComponent: React.FC = ({ ); }; +CaseActionBarComponent.displayName = 'CaseActionBar'; export const CaseActionBar = React.memo(CaseActionBarComponent); diff --git a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx index 193ef4a708e38..dce26fcbd5965 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx @@ -69,5 +69,6 @@ const StatusContextMenuComponent: React.FC = ({ ); }; +StatusContextMenuComponent.displayName = 'StatusContextMenu'; export const StatusContextMenu = memo(StatusContextMenuComponent); diff --git a/x-pack/plugins/cases/public/components/cases_context/index.tsx b/x-pack/plugins/cases/public/components/cases_context/index.tsx index e24a08c38cfeb..aceefad97382a 100644 --- a/x-pack/plugins/cases/public/components/cases_context/index.tsx +++ b/x-pack/plugins/cases/public/components/cases_context/index.tsx @@ -61,6 +61,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ {children} ) : null; }; +CasesProvider.displayName = 'CasesProvider'; function isCasesContextValue(value: CasesContextStateValue): value is CasesContextValue { return value.appId != null && value.appTitle != null && value.userCanCrud != null; diff --git a/x-pack/plugins/cases/public/components/configure_cases/closure_options.tsx b/x-pack/plugins/cases/public/components/configure_cases/closure_options.tsx index 0c76341e9c340..2bcb137d348ad 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/closure_options.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/closure_options.tsx @@ -48,5 +48,6 @@ const ClosureOptionsComponent: React.FC = ({ ); +ClosureOptionsComponent.displayName = 'ClosureOptions'; export const ClosureOptions = React.memo(ClosureOptionsComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/closure_options_radio.tsx b/x-pack/plugins/cases/public/components/configure_cases/closure_options_radio.tsx index cb6fa0953a796..88ee18db805ff 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/closure_options_radio.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/closure_options_radio.tsx @@ -56,5 +56,6 @@ const ClosureOptionsRadioComponent: React.FC /> ); }; +ClosureOptionsRadioComponent.displayName = 'ClosureOptionsRadio'; export const ClosureOptionsRadio = React.memo(ClosureOptionsRadioComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx index 11026acde2bf6..e75f7ed2bdffa 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx @@ -131,5 +131,6 @@ const ConnectorsComponent: React.FC = ({ ); }; +ConnectorsComponent.displayName = 'Connectors'; export const Connectors = React.memo(ConnectorsComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx index af518e3c773b6..9082fc572324e 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx @@ -139,5 +139,6 @@ const ConnectorsDropdownComponent: React.FC = ({ /> ); }; +ConnectorsDropdownComponent.displayName = 'ConnectorsDropdown'; export const ConnectorsDropdown = React.memo(ConnectorsDropdownComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/field_mapping.tsx b/x-pack/plugins/cases/public/components/configure_cases/field_mapping.tsx index fe99f718c1401..7c52184968013 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/field_mapping.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/field_mapping.tsx @@ -62,5 +62,6 @@ const FieldMappingComponent: React.FC = ({ ) : null; }; +FieldMappingComponent.displayName = 'FieldMapping'; export const FieldMapping = React.memo(FieldMappingComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/field_mapping_row_static.tsx b/x-pack/plugins/cases/public/components/configure_cases/field_mapping_row_static.tsx index 09e3546b2e2e3..d7949362e4a1b 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/field_mapping_row_static.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/field_mapping_row_static.tsx @@ -57,5 +57,6 @@ const FieldMappingRowComponent: React.FC = ({ ); }; +FieldMappingRowComponent.displayName = 'FieldMappingRow'; export const FieldMappingRowStatic = React.memo(FieldMappingRowComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx b/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx index eb14c22b900c4..4ce971d652879 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx @@ -56,5 +56,6 @@ const MappingComponent: React.FC = ({ actionTypeName, isLoading, m ); }; +MappingComponent.displayName = 'Mapping'; export const Mapping = React.memo(MappingComponent); diff --git a/x-pack/plugins/cases/public/components/confirm_delete_case/index.tsx b/x-pack/plugins/cases/public/components/confirm_delete_case/index.tsx index baf34daea0d99..ce8287310fb17 100644 --- a/x-pack/plugins/cases/public/components/confirm_delete_case/index.tsx +++ b/x-pack/plugins/cases/public/components/confirm_delete_case/index.tsx @@ -42,5 +42,6 @@ const ConfirmDeleteCaseModalComp: React.FC = ({ ); }; +ConfirmDeleteCaseModalComp.displayName = 'ConfirmDeleteCaseModalComp'; export const ConfirmDeleteCaseModal = React.memo(ConfirmDeleteCaseModalComp); diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.tsx index 05db3474fdb99..83733ea7e97dd 100644 --- a/x-pack/plugins/cases/public/components/connector_selector/form.tsx +++ b/x-pack/plugins/cases/public/components/connector_selector/form.tsx @@ -76,3 +76,4 @@ export const ConnectorSelector = ({ ) : null; }; +ConnectorSelector.displayName = 'ConnectorSelector'; diff --git a/x-pack/plugins/cases/public/components/connectors/card.tsx b/x-pack/plugins/cases/public/components/connectors/card.tsx index 9870c77fda743..d6ef92572e506 100644 --- a/x-pack/plugins/cases/public/components/connectors/card.tsx +++ b/x-pack/plugins/cases/public/components/connectors/card.tsx @@ -77,5 +77,6 @@ const ConnectorCardDisplay: React.FC = ({ ); }; +ConnectorCardDisplay.displayName = 'ConnectorCardDisplay'; export const ConnectorCard = memo(ConnectorCardDisplay); diff --git a/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx b/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx index 7cd9b5f6a367c..d6708657dd08f 100644 --- a/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx @@ -103,6 +103,7 @@ const CaseParamsFields: React.FunctionComponent ); }; +CaseParamsFields.displayName = 'CaseParamsFields'; // eslint-disable-next-line import/no-default-export export { CaseParamsFields as default }; diff --git a/x-pack/plugins/cases/public/components/connectors/case/cases_dropdown.tsx b/x-pack/plugins/cases/public/components/connectors/case/cases_dropdown.tsx index 3f3c7d4931192..c26fa04df6843 100644 --- a/x-pack/plugins/cases/public/components/connectors/case/cases_dropdown.tsx +++ b/x-pack/plugins/cases/public/components/connectors/case/cases_dropdown.tsx @@ -69,5 +69,6 @@ const CasesDropdownComponent: React.FC = ({ ); }; +CasesDropdownComponent.displayName = 'CasesDropdown'; export const CasesDropdown = memo(CasesDropdownComponent); diff --git a/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx b/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx index 33366e11f556a..7472bc6b6047e 100644 --- a/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx +++ b/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx @@ -77,5 +77,6 @@ const ExistingCaseComponent: React.FC = ({ onCaseChanged, sel ); }; +ExistingCaseComponent.displayName = 'ExistingCase'; export const ExistingCase = memo(ExistingCaseComponent); diff --git a/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx b/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx index 195b2deb84d6e..60ae867fab8b4 100644 --- a/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx +++ b/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx @@ -37,5 +37,6 @@ const DeprecatedCalloutComponent: React.FC = ({ type = 'warning' }) => ( {DEPRECATED_CONNECTOR_WARNING_DESC} ); +DeprecatedCalloutComponent.displayName = 'DeprecatedCallout'; export const DeprecatedCallout = React.memo(DeprecatedCalloutComponent); diff --git a/x-pack/plugins/cases/public/components/connectors/fields_form.tsx b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx index 56c56436c08c7..60eb20fe861da 100644 --- a/x-pack/plugins/cases/public/components/connectors/fields_form.tsx +++ b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx @@ -51,5 +51,6 @@ const ConnectorFieldsFormComponent: React.FC = ({ connector, isEdit, onCh ); }; +ConnectorFieldsFormComponent.displayName = 'ConnectorFieldsForm'; export const ConnectorFieldsForm = memo(ConnectorFieldsFormComponent); diff --git a/x-pack/plugins/cases/public/components/connectors/jira/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/jira/case_fields.tsx index b9326a08330cd..1fe02a8483ed3 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/case_fields.tsx @@ -209,6 +209,7 @@ const JiraFieldsComponent: React.FunctionComponent ); }; +JiraFieldsComponent.displayName = 'JiraFields'; // eslint-disable-next-line import/no-default-export export { JiraFieldsComponent as default }; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx b/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx index a9ed87fa81346..53c25db0a72b6 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx @@ -92,5 +92,6 @@ const SearchIssuesComponent: React.FC = ({ selectedValue, actionConnector /> ); }; +SearchIssuesComponent.displayName = 'SearchIssues'; export const SearchIssues = memo(SearchIssuesComponent); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx index a7e584f7c22e2..3ca1d6b3ec674 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -43,6 +43,7 @@ const SwimlaneComponent: React.FunctionComponent ); }; +SwimlaneComponent.displayName = 'Swimlane'; // eslint-disable-next-line import/no-default-export export { SwimlaneComponent as default }; diff --git a/x-pack/plugins/cases/public/components/create/connector.tsx b/x-pack/plugins/cases/public/components/create/connector.tsx index aa0eb024a3b0d..47cc3ea91a868 100644 --- a/x-pack/plugins/cases/public/components/create/connector.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.tsx @@ -64,6 +64,7 @@ const ConnectorFields = ({ /> ); }; +ConnectorFields.displayName = 'ConnectorFields'; const ConnectorComponent: React.FC = ({ connectors, diff --git a/x-pack/plugins/cases/public/components/create/owner_selector.tsx b/x-pack/plugins/cases/public/components/create/owner_selector.tsx index 251681506d516..eaa5382260d21 100644 --- a/x-pack/plugins/cases/public/components/create/owner_selector.tsx +++ b/x-pack/plugins/cases/public/components/create/owner_selector.tsx @@ -101,6 +101,7 @@ const OwnerSelector = ({ ); }; +OwnerSelector.displayName = 'OwnerSelector'; const CaseOwnerSelector: React.FC = ({ availableOwners, isLoading }) => { return ( diff --git a/x-pack/plugins/cases/public/components/create/submit_button.tsx b/x-pack/plugins/cases/public/components/create/submit_button.tsx index b5e58517e6ec1..9f984b236ca69 100644 --- a/x-pack/plugins/cases/public/components/create/submit_button.tsx +++ b/x-pack/plugins/cases/public/components/create/submit_button.tsx @@ -27,5 +27,6 @@ const SubmitCaseButtonComponent: React.FC = () => { ); }; +SubmitCaseButtonComponent.displayName = 'SubmitCaseButton'; export const SubmitCaseButton = memo(SubmitCaseButtonComponent); diff --git a/x-pack/plugins/cases/public/components/formatted_date/index.tsx b/x-pack/plugins/cases/public/components/formatted_date/index.tsx index c286855771794..0fff8431fac44 100644 --- a/x-pack/plugins/cases/public/components/formatted_date/index.tsx +++ b/x-pack/plugins/cases/public/components/formatted_date/index.tsx @@ -142,6 +142,7 @@ export const FormattedRelativePreferenceDate = ({ value }: { value?: string | nu ); }; +FormattedRelativePreferenceDate.displayName = 'FormattedRelativePreferenceDate'; /** * Renders a preceding label according to under/over one hour diff --git a/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap b/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap index 1348e26ebdf6d..86d752f84a8b3 100644 --- a/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap +++ b/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap @@ -9,7 +9,7 @@ exports[`EditableTitle it renders 1`] = ` - diff --git a/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap index a100f5e4f93b4..17517b1d05f19 100644 --- a/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap @@ -8,7 +8,7 @@ exports[`HeaderPage it renders 1`] = ` alignItems="center" > - - diff --git a/x-pack/plugins/cases/public/components/header_page/editable_title.tsx b/x-pack/plugins/cases/public/components/header_page/editable_title.tsx index 43b210b5a5afb..674a31122d983 100644 --- a/x-pack/plugins/cases/public/components/header_page/editable_title.tsx +++ b/x-pack/plugins/cases/public/components/header_page/editable_title.tsx @@ -134,5 +134,6 @@ const EditableTitleComponent: React.FC = ({ ); }; +EditableTitleComponent.displayName = 'EditableTitle'; export const EditableTitle = React.memo(EditableTitleComponent); diff --git a/x-pack/plugins/cases/public/components/header_page/index.tsx b/x-pack/plugins/cases/public/components/header_page/index.tsx index 073e18cd7d728..3afcd15bfa817 100644 --- a/x-pack/plugins/cases/public/components/header_page/index.tsx +++ b/x-pack/plugins/cases/public/components/header_page/index.tsx @@ -127,5 +127,6 @@ const HeaderPageComponent: React.FC = ({ ); }; +HeaderPageComponent.displayName = 'HeaderPage'; export const HeaderPage = React.memo(HeaderPageComponent); diff --git a/x-pack/plugins/cases/public/components/header_page/title.tsx b/x-pack/plugins/cases/public/components/header_page/title.tsx index 18b10c4f7bbcb..9ccf13b8d83a9 100644 --- a/x-pack/plugins/cases/public/components/header_page/title.tsx +++ b/x-pack/plugins/cases/public/components/header_page/title.tsx @@ -52,5 +52,6 @@ const TitleComponent: React.FC = ({ title, badgeOptions }) => ( ); +TitleComponent.displayName = 'Title'; export const Title = React.memo(TitleComponent); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/markdown_link.tsx b/x-pack/plugins/cases/public/components/markdown_editor/markdown_link.tsx index 7cc8a07c8c04e..c42ef90648bc9 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/markdown_link.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/markdown_link.tsx @@ -31,5 +31,6 @@ const MarkdownLinkComponent: React.FC = ({ ); +MarkdownLinkComponent.displayName = 'MarkdownLink'; export const MarkdownLink = memo(MarkdownLinkComponent); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx index 0d3a62d312701..5179aed6518b5 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx @@ -375,6 +375,7 @@ const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({ ); }; +LensEditorComponent.displayName = 'LensEditor'; export const LensEditor = React.memo(LensEditorComponent); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx index 9e39cc5cb8218..1ca3f53291eb0 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx @@ -55,5 +55,6 @@ const LensMarkDownRendererComponent: React.FC = ({ ); }; +LensMarkDownRendererComponent.displayName = 'LensMarkDownRenderer'; export const LensMarkDownRenderer = React.memo(LensMarkDownRendererComponent); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx b/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx index b0f167628496b..e0265a2884b97 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx @@ -46,5 +46,6 @@ const MarkdownRendererComponent: React.FC = ({ children, disableLinks }) ); }; +MarkdownRendererComponent.displayName = 'MarkdownRenderer'; export const MarkdownRenderer = memo(MarkdownRendererComponent); diff --git a/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx b/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx index 794984c08d79c..0f9111e5acd1d 100644 --- a/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx +++ b/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx @@ -91,3 +91,4 @@ export const RecentCasesComp = ({ filterOptions, maxCasesToShow }: RecentCasesPr ); }; +RecentCasesComp.displayName = 'RecentCasesComp'; diff --git a/x-pack/plugins/cases/public/components/status/button.tsx b/x-pack/plugins/cases/public/components/status/button.tsx index c9dcd509c1002..34eb5d62c7790 100644 --- a/x-pack/plugins/cases/public/components/status/button.tsx +++ b/x-pack/plugins/cases/public/components/status/button.tsx @@ -42,4 +42,5 @@ const StatusActionButtonComponent: React.FC = ({ status, onStatusChanged, ); }; +StatusActionButtonComponent.displayName = 'StatusActionButton'; export const StatusActionButton = memo(StatusActionButtonComponent); diff --git a/x-pack/plugins/cases/public/components/status/status.tsx b/x-pack/plugins/cases/public/components/status/status.tsx index 47c30a7761264..ad9add5d3fffd 100644 --- a/x-pack/plugins/cases/public/components/status/status.tsx +++ b/x-pack/plugins/cases/public/components/status/status.tsx @@ -46,5 +46,6 @@ const StatusComponent: React.FC = ({ ); }; +StatusComponent.displayName = 'Status'; export const Status = memo(StatusComponent); diff --git a/x-pack/plugins/cases/public/components/tag_list/tags.tsx b/x-pack/plugins/cases/public/components/tag_list/tags.tsx index f3b05972a24a9..ec8a84de1aa88 100644 --- a/x-pack/plugins/cases/public/components/tag_list/tags.tsx +++ b/x-pack/plugins/cases/public/components/tag_list/tags.tsx @@ -30,5 +30,6 @@ const TagsComponent: React.FC = ({ tags, color = 'default', gutterSiz )} ); +TagsComponent.displayName = 'Tags'; export const Tags = memo(TagsComponent); diff --git a/x-pack/plugins/cases/public/components/truncated_text/index.tsx b/x-pack/plugins/cases/public/components/truncated_text/index.tsx index 3cf7f8322d797..7d663dc989c96 100644 --- a/x-pack/plugins/cases/public/components/truncated_text/index.tsx +++ b/x-pack/plugins/cases/public/components/truncated_text/index.tsx @@ -26,5 +26,6 @@ interface Props { const TruncatedTextComponent: React.FC = ({ text }) => { return {text}; }; +TruncatedTextComponent.displayName = 'TruncatedText'; export const TruncatedText = React.memo(TruncatedTextComponent); diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx index f3badc94e5468..6c844a660b801 100644 --- a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx +++ b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx @@ -40,6 +40,7 @@ const CreateModalComponent: React.FC = ({ ) : null; +CreateModalComponent.displayName = 'CreateModal'; export const CreateCaseModal = memo(CreateModalComponent); diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.tsx index 4c27d8ce7f87b..a6acf692be10e 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.tsx @@ -68,5 +68,6 @@ const CallOutComponent = ({ ) : null; }; +CallOutComponent.displayName = 'CallOut'; export const CallOut = memo(CallOutComponent); diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/callout/index.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/callout/index.tsx index 40c61175153c0..fb9145d2a941a 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/callout/index.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/callout/index.tsx @@ -89,5 +89,6 @@ const CaseCallOutComponent = ({ ); }; +CaseCallOutComponent.displayName = 'CaseCallOut'; export const CaseCallOut = memo(CaseCallOutComponent); diff --git a/x-pack/plugins/cases/public/components/user_actions/avatar.tsx b/x-pack/plugins/cases/public/components/user_actions/avatar.tsx index da43a122f3868..a8cfb7a3b2acf 100644 --- a/x-pack/plugins/cases/public/components/user_actions/avatar.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/avatar.tsx @@ -20,5 +20,6 @@ const UserActionAvatarComponent = ({ username, fullName, size = 'm' }: UserActio const avatarName = fullName && fullName.length > 0 ? fullName : username ?? i18n.UNKNOWN; return ; }; +UserActionAvatarComponent.displayName = 'UserActionAvatar'; export const UserActionAvatar = memo(UserActionAvatarComponent); diff --git a/x-pack/plugins/cases/public/components/user_actions/avatar_username.tsx b/x-pack/plugins/cases/public/components/user_actions/avatar_username.tsx index 581ebb7272d34..1a724838b5207 100644 --- a/x-pack/plugins/cases/public/components/user_actions/avatar_username.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/avatar_username.tsx @@ -34,5 +34,6 @@ const UserActionUsernameWithAvatarComponent = ({ ); +UserActionUsernameWithAvatarComponent.displayName = 'UserActionUsernameWithAvatar'; export const UserActionUsernameWithAvatar = memo(UserActionUsernameWithAvatarComponent); diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.tsx index b4b4b3b75fe7e..03e4c9b56d0c6 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.tsx @@ -63,5 +63,6 @@ const AlertCommentEventComponent: React.FC = ({ ); }; +AlertCommentEventComponent.displayName = 'AlertCommentEvent'; export const AlertCommentEvent = memo(AlertCommentEventComponent); diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/host_isolation_event.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/host_isolation_event.tsx index 531323e548dd1..e08c2f85e8c31 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/host_isolation_event.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/host_isolation_event.tsx @@ -52,5 +52,6 @@ const HostIsolationCommentEventComponent: React.FC = ({ ); }; +HostIsolationCommentEventComponent.displayName = 'HostIsolationCommentEvent'; export const HostIsolationCommentEvent = memo(HostIsolationCommentEventComponent); diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/show_alert.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/show_alert.tsx index dd874b029dc9c..5506cbd5d7d00 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/show_alert.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/show_alert.tsx @@ -38,5 +38,6 @@ const UserActionShowAlertComponent = ({ ); }; +UserActionShowAlertComponent.displayName = 'UserActionShowAlert'; export const UserActionShowAlert = memo(UserActionShowAlertComponent); diff --git a/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx index dee1a25c3b79c..0a1f02b4aaebf 100644 --- a/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx @@ -50,5 +50,6 @@ const UserActionContentToolbarComponent = ({ ); +UserActionContentToolbarComponent.displayName = 'UserActionContentToolbar'; export const UserActionContentToolbar = memo(UserActionContentToolbarComponent); diff --git a/x-pack/plugins/cases/public/components/user_actions/copy_link.tsx b/x-pack/plugins/cases/public/components/user_actions/copy_link.tsx index 54c9b36c53962..3a74b2024db3e 100644 --- a/x-pack/plugins/cases/public/components/user_actions/copy_link.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/copy_link.tsx @@ -36,5 +36,6 @@ const UserActionCopyLinkComponent = ({ id: commentId }: UserActionCopyLinkProps) ); }; +UserActionCopyLinkComponent.displayName = 'UserActionCopyLink'; export const UserActionCopyLink = memo(UserActionCopyLinkComponent); diff --git a/x-pack/plugins/cases/public/components/user_actions/move_to_reference.tsx b/x-pack/plugins/cases/public/components/user_actions/move_to_reference.tsx index 42f6031ba1a6e..34647d7ce6b99 100644 --- a/x-pack/plugins/cases/public/components/user_actions/move_to_reference.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/move_to_reference.tsx @@ -34,5 +34,6 @@ const UserActionMoveToReferenceComponent = ({ ); }; +UserActionMoveToReferenceComponent.displayName = 'UserActionMoveToReference'; export const UserActionMoveToReference = memo(UserActionMoveToReferenceComponent); diff --git a/x-pack/plugins/cases/public/components/user_actions/property_actions.tsx b/x-pack/plugins/cases/public/components/user_actions/property_actions.tsx index 8f89c3b420801..86e1ac0e1f481 100644 --- a/x-pack/plugins/cases/public/components/user_actions/property_actions.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/property_actions.tsx @@ -69,5 +69,6 @@ const UserActionPropertyActionsComponent = ({ ); }; +UserActionPropertyActionsComponent.displayName = 'UserActionPropertyActions'; export const UserActionPropertyActions = memo(UserActionPropertyActionsComponent); diff --git a/x-pack/plugins/cases/public/components/user_actions/timestamp.tsx b/x-pack/plugins/cases/public/components/user_actions/timestamp.tsx index 98e25fe265dbb..a8c38657d1ee8 100644 --- a/x-pack/plugins/cases/public/components/user_actions/timestamp.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/timestamp.tsx @@ -41,5 +41,6 @@ const UserActionTimestampComponent = ({ createdAt, updatedAt }: UserActionAvatar )} ); +UserActionTimestampComponent.displayName = 'UserActionTimestamp'; export const UserActionTimestamp = memo(UserActionTimestampComponent); diff --git a/x-pack/plugins/cases/public/components/user_actions/username.tsx b/x-pack/plugins/cases/public/components/user_actions/username.tsx index 78309eb56d620..52604d1374951 100644 --- a/x-pack/plugins/cases/public/components/user_actions/username.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/username.tsx @@ -28,5 +28,6 @@ const UserActionUsernameComponent = ({ username, fullName }: UserActionUsernameP ); }; +UserActionUsernameComponent.displayName = 'UserActionUsername'; export const UserActionUsername = memo(UserActionUsernameComponent); From 022a9efa5e8d63fdec62118efe284d7af4540ed3 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Thu, 20 Jan 2022 20:28:09 +0100 Subject: [PATCH 17/43] Save github PR list in csv (#123276) * save PR list for release testing in csv * use paginate to simplify code * fix * Update src/dev/github/get_prs_cli.ts Co-authored-by: Spencer * review fix * update example with OR logic * fix optional flags check Co-authored-by: Spencer Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- scripts/download_pr_list.js | 10 +++ src/dev/github/download_pr_list_cli.ts | 79 ++++++++++++++++++++++ src/dev/github/example.json | 12 ++++ src/dev/github/search_and_save_pr_list.ts | 82 +++++++++++++++++++++++ 4 files changed, 183 insertions(+) create mode 100644 scripts/download_pr_list.js create mode 100644 src/dev/github/download_pr_list_cli.ts create mode 100644 src/dev/github/example.json create mode 100644 src/dev/github/search_and_save_pr_list.ts diff --git a/scripts/download_pr_list.js b/scripts/download_pr_list.js new file mode 100644 index 0000000000000..d0ee65d95d2d3 --- /dev/null +++ b/scripts/download_pr_list.js @@ -0,0 +1,10 @@ +/* + * 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. + */ + +require('../src/setup_node_env'); +require('../src/dev/github/download_pr_list_cli').downloadPullRequests(); diff --git a/src/dev/github/download_pr_list_cli.ts b/src/dev/github/download_pr_list_cli.ts new file mode 100644 index 0000000000000..fed7bc8b4f086 --- /dev/null +++ b/src/dev/github/download_pr_list_cli.ts @@ -0,0 +1,79 @@ +/* + * 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 { run, createFlagError, Flags } from '@kbn/dev-utils'; +import fs from 'fs'; +import Path from 'path'; +import { savePrsToCsv } from './search_and_save_pr_list'; + +function getLabelsPath(flags: Flags) { + if (typeof flags.path !== 'string') { + throw createFlagError('please provide a single --path flag'); + } + + if (!fs.existsSync(Path.resolve(flags.path))) { + throw createFlagError('please provide an existing json file with --path flag'); + } + + return Path.resolve(flags.path); +} + +export async function downloadPullRequests() { + run( + async ({ log, flags }) => { + const githubToken = process.env.GITHUB_TOKEN; + + if (!githubToken) { + throw new Error('GITHUB_TOKEN was not provided.'); + } + + const labelsPath = getLabelsPath(flags); + + if (typeof flags.dest !== 'string') { + throw createFlagError('please provide csv path in --dest flag'); + } + + const query = flags.query || undefined; + if (query !== undefined && typeof query !== 'string') { + throw createFlagError('please provide valid string in --query flag'); + } + + const mergedSince = flags['merged-since'] || undefined; + if ( + mergedSince !== undefined && + (typeof mergedSince !== 'string' || !/\d{4}-\d{2}-\d{2}/.test(mergedSince)) + ) { + throw createFlagError( + `please provide a past date in 'yyyy-mm-dd' format in --merged-since flag` + ); + } + + fs.mkdirSync(flags.dest, { recursive: true }); + const filename = Path.resolve( + flags.dest, + `kibana_prs_${new Date().toISOString().split('T').join('-')}.csv` + ); + await savePrsToCsv(log, githubToken, labelsPath, filename, query, mergedSince); + }, + { + description: ` + Create a csv file with PRs to be tests for upcoming release, + require GITHUB_TOKEN variable to be set in advance + `, + flags: { + string: ['path', 'dest', 'query', 'merged-since'], + help: ` + --path Required, path to json file with labels to operate on, see src/dev/example.json + --dest Required, generated csv file location + --query Optional, overrides default query + --merged-since Optional, start date in 'yyyy-mm-dd' format + `, + }, + } + ); +} diff --git a/src/dev/github/example.json b/src/dev/github/example.json new file mode 100644 index 0000000000000..34a74eefb3785 --- /dev/null +++ b/src/dev/github/example.json @@ -0,0 +1,12 @@ +{ + "include": [ + "v8.0.0", + "\"Team:ResponseOps\",\"Feature:Canvas\"" + ], + "exclude": [ + "failed-test", + "Feature:Unit Testing", + "Feature:Functional Testing", + "release_note:skip" + ] +} \ No newline at end of file diff --git a/src/dev/github/search_and_save_pr_list.ts b/src/dev/github/search_and_save_pr_list.ts new file mode 100644 index 0000000000000..1c6dbc6c99746 --- /dev/null +++ b/src/dev/github/search_and_save_pr_list.ts @@ -0,0 +1,82 @@ +/* + * 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 { ToolingLog } from '@kbn/dev-utils'; +import { Octokit } from '@octokit/rest'; +import fs from 'fs'; + +interface Labels { + include: string[]; + exclude: string[]; +} + +interface PR { + title: string; + url: string; + releaseLabel: string; +} + +export async function savePrsToCsv( + log: ToolingLog, + githubToken: string, + labelsPath: string, + filename: string, + query: string | undefined, + mergedSince: string | undefined +) { + const repo = `repo:"elastic/kibana"`; + const defaultQuery = 'is:pull-request+is:merged+sort:updated-desc'; + const perPage = 100; + const searchApiLimit = 1000; + + let q = repo + '+' + (query ?? defaultQuery) + (mergedSince ? `+merged:>=${mergedSince}` : ''); + + const rawData = fs.readFileSync(labelsPath, 'utf8'); + const labels = JSON.parse(rawData) as Labels; + + labels.include.map((label) => (q += `+label:${label}`)); + labels.exclude.map((label) => (q += ` -label:"${label}"`)); + + log.debug(`Github query: ${q}`); + + const octokit = new Octokit({ + auth: githubToken, + }); + const items: PR[] = await octokit.paginate( + 'GET /search/issues', + { q, per_page: perPage }, + (response) => + response.data.map((item: Octokit.SearchIssuesAndPullRequestsResponseItemsItem) => { + return { + title: item.title, + url: item.html_url, + releaseLabel: item.labels + .filter((label) => label.name.trim().startsWith('release_note')) + .map((label) => label.name) + .join(','), + } as PR; + }) + ); + + // https://docs.github.com/en/rest/reference/search + if (items.length >= searchApiLimit) { + log.warning( + `Search API limit is 1000 results per search, try to adjust the query. Saving first 1000 PRs` + ); + } else { + log.info(`Found ${items.length} PRs`); + } + + let csv = ''; + for (const i of items) { + csv += `${i.title}\t${i.url}\t${i.releaseLabel}\r\n`; + } + + fs.writeFileSync(filename, csv); + log.info(`Saved to ${filename}`); +} From 6ae646722b2f93044477264a927b27705967a4ab Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Thu, 20 Jan 2022 20:46:52 +0100 Subject: [PATCH 18/43] [Expressions] Add support of comments (#122457) * Add comments support to the expressions grammar * Add typings to the interpreter parser * Add expressions comments highlighting * Update canvas to preserve original expression formatting * Update documentation to cover comments --- .../canvas-expression-lifecycle.asciidoc | 2 + docs/developer/plugin-list.asciidoc | 3 + .../kbn-interpreter/grammar/grammar.peggy | 22 ++- packages/kbn-interpreter/src/common/index.ts | 22 ++- .../kbn-interpreter/src/common/lib/ast.ts | 159 ------------------ .../kbn-interpreter/src/common/lib/ast/ast.ts | 64 +++++++ .../src/common/lib/ast/compare.ts | 85 ++++++++++ .../from_expression.test.js} | 4 +- .../src/common/lib/ast/from_expression.ts | 18 ++ .../src/common/lib/ast/index.ts | 12 ++ .../src/common/lib/ast/patch.ts | 47 ++++++ .../lib/ast/safe_element_from_expression.ts | 26 +++ .../to_expression.test.js} | 109 +++++++++++- .../src/common/lib/ast/to_expression.ts | 145 ++++++++++++++++ .../src/common/lib/grammar.d.ts | 11 ++ .../kbn-interpreter/src/common/lib/parse.ts | 27 +++ src/plugins/expressions/README.asciidoc | 6 +- src/plugins/expressions/common/ast/types.ts | 8 +- .../expression_input/autocomplete.ts | 76 ++------- .../components/expression_input/language.ts | 9 + .../hooks/use_canvas_filters.ts | 4 +- x-pack/plugins/canvas/public/lib/filter.ts | 8 +- .../canvas/public/lib/filter_adapters.test.ts | 4 +- .../canvas/public/lib/filter_adapters.ts | 6 +- .../server/collectors/collector_helpers.ts | 16 +- .../saved_objects/workpad_references.ts | 7 +- .../editor_frame/expression_helpers.ts | 4 +- .../definitions/calculations/utils.ts | 6 +- .../migrations/saved_object_migrations.ts | 6 +- 29 files changed, 644 insertions(+), 272 deletions(-) delete mode 100644 packages/kbn-interpreter/src/common/lib/ast.ts create mode 100644 packages/kbn-interpreter/src/common/lib/ast/ast.ts create mode 100644 packages/kbn-interpreter/src/common/lib/ast/compare.ts rename packages/kbn-interpreter/src/common/lib/{ast.from_expression.test.js => ast/from_expression.test.js} (98%) create mode 100644 packages/kbn-interpreter/src/common/lib/ast/from_expression.ts create mode 100644 packages/kbn-interpreter/src/common/lib/ast/index.ts create mode 100644 packages/kbn-interpreter/src/common/lib/ast/patch.ts create mode 100644 packages/kbn-interpreter/src/common/lib/ast/safe_element_from_expression.ts rename packages/kbn-interpreter/src/common/lib/{ast.to_expression.test.js => ast/to_expression.test.js} (84%) create mode 100644 packages/kbn-interpreter/src/common/lib/ast/to_expression.ts create mode 100644 packages/kbn-interpreter/src/common/lib/grammar.d.ts create mode 100644 packages/kbn-interpreter/src/common/lib/parse.ts diff --git a/docs/canvas/canvas-expression-lifecycle.asciidoc b/docs/canvas/canvas-expression-lifecycle.asciidoc index 17903408dff0e..a20181c4b3808 100644 --- a/docs/canvas/canvas-expression-lifecycle.asciidoc +++ b/docs/canvas/canvas-expression-lifecycle.asciidoc @@ -14,6 +14,7 @@ To use demo dataset available in Canvas to produce a table, run the following ex [source,text] ---- +/* Simple demo table */ filters | demodata | table @@ -24,6 +25,7 @@ This expression starts out with the <> function, which prov The filtered <> becomes the _context_ of the next function, <>, which creates a table visualization from this data set. The <> function isn’t strictly required, but by being explicit, you have the option of providing arguments to control things like the font used in the table. The output of the <> function becomes the _context_ of the <> function. Like the <>, the <> function isn’t required either, but it allows access to other arguments, such as styling the border of the element or injecting custom CSS. +It is possible to add comments to the expression by starting them with a `//` sequence or by using `/*` and `*/` to enclose multi-line comments. [[canvas-function-arguments]] === Function arguments diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 5a6773156bfba..fa7613049f9d6 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -140,6 +140,9 @@ All the arguments to expression functions need to be serializable, as well as in Expression functions should try to stay 'pure'. This makes functions easy to reuse and also make it possible to serialize the whole chain as well as output at every step of execution. +It is possible to add comments to expressions by starting them with a `//` sequence +or by using `/*` and `*/` to enclose multi-line comments. + Expressions power visualizations in Dashboard and Lens, as well as, every *element* in Canvas is backed by an expression. diff --git a/packages/kbn-interpreter/grammar/grammar.peggy b/packages/kbn-interpreter/grammar/grammar.peggy index e8a2988d2b4f6..e67e310c9f4d1 100644 --- a/packages/kbn-interpreter/grammar/grammar.peggy +++ b/packages/kbn-interpreter/grammar/grammar.peggy @@ -23,7 +23,7 @@ start = expression expression - = space? first:function? rest:('|' space? fn:function { return fn; })* { + = blank? first:function? rest:('|' blank? fn:function { return fn; })* { return addMeta({ type: 'expression', chain: first ? [first].concat(rest) : [] @@ -44,7 +44,7 @@ function "function" /* ----- Arguments ----- */ argument_assignment - = name:identifier space? '=' space? value:argument { + = name:identifier blank? '=' blank? value:argument { return { name, value }; } / value:argument { @@ -58,7 +58,7 @@ argument } arg_list - = args:(space arg:argument_assignment { return arg; })* space? { + = args:(blank arg:argument_assignment { return arg; })* blank? { return args.reduce((accumulator, { name, value }) => ({ ...accumulator, [name]: (accumulator[name] || []).concat(value) @@ -82,7 +82,7 @@ phrase unquoted_string_or_number // Make sure we're not matching the beginning of a search - = string:unquoted+ { // this also matches nulls, booleans, and numbers + = !comment string:unquoted+ { // this also matches nulls, booleans, and numbers var result = string.join(''); // Sort of hacky, but PEG doesn't have backtracking so // a null/boolean/number rule is hard to read, and performs worse @@ -93,8 +93,20 @@ unquoted_string_or_number return Number(result); } +blank + = (space / comment)+ + space - = [\ \t\r\n]+ + = [\ \t\r\n] + +comment + = inline_comment / multiline_comment + +inline_comment + = "//" [^\n]* + +multiline_comment + = "/*" (!"*/" .)* "*/"? unquoted = "\\" sequence:([\"'(){}<>\[\]$`|=\ \t\n\r] / "\\") { return sequence; } diff --git a/packages/kbn-interpreter/src/common/index.ts b/packages/kbn-interpreter/src/common/index.ts index 982de97cf881b..52753286d7221 100644 --- a/packages/kbn-interpreter/src/common/index.ts +++ b/packages/kbn-interpreter/src/common/index.ts @@ -6,14 +6,26 @@ * Side Public License, v 1. */ -export type { Ast, ExpressionFunctionAST } from './lib/ast'; -export { fromExpression, toExpression, safeElementFromExpression } from './lib/ast'; +export type { + Ast, + AstArgument, + AstFunction, + AstNode, + AstWithMeta, + AstArgumentWithMeta, + AstFunctionWithMeta, +} from './lib/ast'; +export { + fromExpression, + isAst, + isAstWithMeta, + toExpression, + safeElementFromExpression, +} from './lib/ast'; export { Fn } from './lib/fn'; export { getType } from './lib/get_type'; export { castProvider } from './lib/cast'; -// @ts-expect-error -// @internal -export { parse } from '../../grammar'; +export { parse } from './lib/parse'; export { getByAlias } from './lib/get_by_alias'; export { Registry } from './lib/registry'; export { addRegistries, register, registryFactory } from './registries'; diff --git a/packages/kbn-interpreter/src/common/lib/ast.ts b/packages/kbn-interpreter/src/common/lib/ast.ts deleted file mode 100644 index 791c94809f35c..0000000000000 --- a/packages/kbn-interpreter/src/common/lib/ast.ts +++ /dev/null @@ -1,159 +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 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 { getType } from './get_type'; -// @ts-expect-error -import { parse } from '../../../grammar'; - -export type ExpressionArgAST = string | boolean | number | Ast; - -export interface ExpressionFunctionAST { - type: 'function'; - function: string; - arguments: { - [key: string]: ExpressionArgAST[]; - }; -} - -export interface Ast { - /** @internal */ - function: any; - /** @internal */ - arguments: any; - type: 'expression'; - chain: ExpressionFunctionAST[]; - /** @internal */ - replace(regExp: RegExp, s: string): string; -} - -function getArgumentString(arg: Ast, argKey: string | undefined, level = 0) { - const type = getType(arg); - - // eslint-disable-next-line @typescript-eslint/no-shadow - function maybeArgKey(argKey: string | null | undefined, argString: string) { - return argKey == null || argKey === '_' ? argString : `${argKey}=${argString}`; - } - - if (type === 'string') { - // correctly (re)escape double quotes - const escapedArg = arg.replace(/[\\"]/g, '\\$&'); // $& means the whole matched string - return maybeArgKey(argKey, `"${escapedArg}"`); - } - - if (type === 'boolean' || type === 'null' || type === 'number') { - // use values directly - return maybeArgKey(argKey, `${arg}`); - } - - if (type === 'expression') { - // build subexpressions - return maybeArgKey(argKey, `{${getExpression(arg.chain, level + 1)}}`); - } - - // unknown type, throw with type value - throw new Error(`Invalid argument type in AST: ${type}`); -} - -function getExpressionArgs(block: Ast, level = 0) { - const args = block.arguments; - const hasValidArgs = typeof args === 'object' && args != null && !Array.isArray(args); - - if (!hasValidArgs) throw new Error('Arguments can only be an object'); - - const argKeys = Object.keys(args); - const MAX_LINE_LENGTH = 80; // length before wrapping arguments - return argKeys.map((argKey) => - args[argKey].reduce((acc: any, arg: any) => { - const argString = getArgumentString(arg, argKey, level); - const lineLength = acc.split('\n').pop().length; - - // if arg values are too long, move it to the next line - if (level === 0 && lineLength + argString.length > MAX_LINE_LENGTH) { - return `${acc}\n ${argString}`; - } - - // append arg values to existing arg values - if (lineLength > 0) return `${acc} ${argString}`; - - // start the accumulator with the first arg value - return argString; - }, '') - ); -} - -function fnWithArgs(fnName: any, args: any[]) { - if (!args || args.length === 0) return fnName; - return `${fnName} ${args.join(' ')}`; -} - -function getExpression(chain: any[], level = 0) { - if (!chain) throw new Error('Expressions must contain a chain'); - - // break new functions onto new lines if we're not in a nested/sub-expression - const separator = level > 0 ? ' | ' : '\n| '; - - return chain - .map((chainObj) => { - const type = getType(chainObj); - - if (type === 'function') { - const fn = chainObj.function; - if (!fn || fn.length === 0) throw new Error('Functions must have a function name'); - - const expArgs = getExpressionArgs(chainObj, level); - - return fnWithArgs(fn, expArgs); - } - }, []) - .join(separator); -} - -export function fromExpression(expression: string, type = 'expression'): Ast { - try { - return parse(String(expression), { startRule: type }); - } catch (e) { - throw new Error(`Unable to parse expression: ${e.message}`); - } -} - -// TODO: OMG This is so bad, we need to talk about the right way to handle bad expressions since some are element based and others not -export function safeElementFromExpression(expression: string) { - try { - return fromExpression(expression); - } catch (e) { - return fromExpression( - `markdown -"## Crud. -Canvas could not parse this element's expression. I am so sorry this error isn't more useful. I promise it will be soon. - -Thanks for understanding, -#### Management -"` - ); - } -} - -// TODO: Respect the user's existing formatting -export function toExpression(astObj: Ast, type = 'expression'): string { - if (type === 'argument') { - // @ts-ignore - return getArgumentString(astObj); - } - - const validType = ['expression', 'function'].includes(getType(astObj)); - if (!validType) throw new Error('Expression must be an expression or argument function'); - - if (getType(astObj) === 'expression') { - if (!Array.isArray(astObj.chain)) throw new Error('Expressions must contain a chain'); - - return getExpression(astObj.chain); - } - - const expArgs = getExpressionArgs(astObj); - return fnWithArgs(astObj.function, expArgs); -} diff --git a/packages/kbn-interpreter/src/common/lib/ast/ast.ts b/packages/kbn-interpreter/src/common/lib/ast/ast.ts new file mode 100644 index 0000000000000..f0ea42cc039e4 --- /dev/null +++ b/packages/kbn-interpreter/src/common/lib/ast/ast.ts @@ -0,0 +1,64 @@ +/* + * 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. + */ + +export type AstNode = Ast | AstFunction | AstArgument; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type Ast = { + type: 'expression'; + chain: AstFunction[]; +}; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type AstFunction = { + type: 'function'; + function: string; + arguments: Record; +}; + +export type AstArgument = string | boolean | number | Ast; + +interface WithMeta { + start: number; + end: number; + text: string; + node: T; +} + +type Replace = Pick> & R; + +type WrapAstArgumentWithMeta = T extends Ast ? AstWithMeta : WithMeta; +export type AstArgumentWithMeta = WrapAstArgumentWithMeta; + +export type AstFunctionWithMeta = WithMeta< + Replace< + AstFunction, + { + arguments: { + [key: string]: AstArgumentWithMeta[]; + }; + } + > +>; + +export type AstWithMeta = WithMeta< + Replace< + Ast, + { + chain: AstFunctionWithMeta[]; + } + > +>; + +export function isAstWithMeta(value: any): value is AstWithMeta { + return typeof value?.node === 'object'; +} + +export function isAst(value: any): value is Ast { + return typeof value === 'object' && value?.type === 'expression'; +} diff --git a/packages/kbn-interpreter/src/common/lib/ast/compare.ts b/packages/kbn-interpreter/src/common/lib/ast/compare.ts new file mode 100644 index 0000000000000..e9a9772dba227 --- /dev/null +++ b/packages/kbn-interpreter/src/common/lib/ast/compare.ts @@ -0,0 +1,85 @@ +/* + * 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 { forEach, xor, zip } from 'lodash'; +import { parse } from '../parse'; +import type { + Ast, + AstArgument, + AstArgumentWithMeta, + AstWithMeta, + AstFunction, + AstFunctionWithMeta, +} from './ast'; +import { isAst, isAstWithMeta } from './ast'; + +export interface ValueChange { + type: 'value'; + source: AstArgumentWithMeta; + target: AstArgument; +} + +export type Change = ValueChange; + +export function isValueChange(value: any): value is ValueChange { + return value?.type === 'value'; +} + +export function compare(expression: string, ast: Ast): Change[] { + const astWithMeta = parse(expression, { addMeta: true }); + const queue = [[astWithMeta, ast]] as Array<[typeof astWithMeta, typeof ast]>; + const changes = [] as Change[]; + + function compareExpression(source: AstWithMeta, target: Ast) { + zip(source.node.chain, target.chain).forEach(([fnWithMeta, fn]) => { + if (!fnWithMeta || !fn || fnWithMeta?.node.function !== fn?.function) { + throw Error('Expression changes are not supported.'); + } + + compareFunction(fnWithMeta, fn); + }); + } + + function compareFunction(source: AstFunctionWithMeta, target: AstFunction) { + if (xor(Object.keys(source.node.arguments), Object.keys(target.arguments)).length) { + throw Error('Function changes are not supported.'); + } + + forEach(source.node.arguments, (valuesWithMeta, argument) => { + const values = target.arguments[argument]; + + compareArgument(valuesWithMeta, values); + }); + } + + function compareArgument(source: AstArgumentWithMeta[], target: AstArgument[]) { + if (source.length !== target.length) { + throw Error('Arguments changes are not supported.'); + } + + zip(source, target).forEach(([valueWithMeta, value]) => compareValue(valueWithMeta!, value!)); + } + + function compareValue(source: AstArgumentWithMeta, target: AstArgument) { + if (isAstWithMeta(source) && isAst(target)) { + compareExpression(source, target); + + return; + } + + if (source.node !== target) { + changes.push({ type: 'value', source, target }); + } + } + + while (queue.length) { + compareExpression(...queue.shift()!); + } + + return changes; +} diff --git a/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js b/packages/kbn-interpreter/src/common/lib/ast/from_expression.test.js similarity index 98% rename from packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js rename to packages/kbn-interpreter/src/common/lib/ast/from_expression.test.js index 11a25f6250cd6..24a652e29bb09 100644 --- a/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js +++ b/packages/kbn-interpreter/src/common/lib/ast/from_expression.test.js @@ -7,9 +7,9 @@ */ import { fromExpression } from '@kbn/interpreter'; -import { getType } from './get_type'; +import { getType } from '../get_type'; -describe('ast fromExpression', () => { +describe('fromExpression', () => { describe('invalid expression', () => { it('throws with invalid expression', () => { const check = () => fromExpression('wat!'); diff --git a/packages/kbn-interpreter/src/common/lib/ast/from_expression.ts b/packages/kbn-interpreter/src/common/lib/ast/from_expression.ts new file mode 100644 index 0000000000000..859afd994f744 --- /dev/null +++ b/packages/kbn-interpreter/src/common/lib/ast/from_expression.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 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 type { Ast } from './ast'; +import { parse } from '../parse'; + +export function fromExpression(expression: string, type = 'expression'): Ast { + try { + return parse(String(expression), { startRule: type }); + } catch (e) { + throw new Error(`Unable to parse expression: ${e.message}`); + } +} diff --git a/packages/kbn-interpreter/src/common/lib/ast/index.ts b/packages/kbn-interpreter/src/common/lib/ast/index.ts new file mode 100644 index 0000000000000..0889a21af21a8 --- /dev/null +++ b/packages/kbn-interpreter/src/common/lib/ast/index.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +export * from './ast'; +export * from './from_expression'; +export * from './safe_element_from_expression'; +export * from './to_expression'; diff --git a/packages/kbn-interpreter/src/common/lib/ast/patch.ts b/packages/kbn-interpreter/src/common/lib/ast/patch.ts new file mode 100644 index 0000000000000..1dc6ef2cff6ad --- /dev/null +++ b/packages/kbn-interpreter/src/common/lib/ast/patch.ts @@ -0,0 +1,47 @@ +/* + * 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 type { Ast } from './ast'; +import { isAstWithMeta } from './ast'; +import type { Change, ValueChange } from './compare'; +import { compare, isValueChange } from './compare'; +import { toExpression } from './to_expression'; + +export function patch(expression: string, ast: Ast): string { + let result = ''; + let position = 0; + + function apply(change: Change) { + if (isValueChange(change)) { + return void patchValue(change); + } + + throw new Error('Cannot apply patch for the change.'); + } + + function patchValue(change: ValueChange) { + if (isAstWithMeta(change.source)) { + throw new Error('Patching sub-expressions is not supported.'); + } + + result += `${expression.substring(position, change.source.start)}${toExpression( + change.target, + 'argument' + )}`; + + position = change.source.end; + } + + compare(expression, ast) + .sort(({ source: source1 }, { source: source2 }) => source1.start - source2.start) + .forEach(apply); + + result += expression.substring(position); + + return result; +} diff --git a/packages/kbn-interpreter/src/common/lib/ast/safe_element_from_expression.ts b/packages/kbn-interpreter/src/common/lib/ast/safe_element_from_expression.ts new file mode 100644 index 0000000000000..3685847989c8f --- /dev/null +++ b/packages/kbn-interpreter/src/common/lib/ast/safe_element_from_expression.ts @@ -0,0 +1,26 @@ +/* + * 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 { fromExpression } from './from_expression'; + +// TODO: OMG This is so bad, we need to talk about the right way to handle bad expressions since some are element based and others not +export function safeElementFromExpression(expression: string) { + try { + return fromExpression(expression); + } catch (e) { + return fromExpression( + `markdown +"## Crud. +Canvas could not parse this element's expression. I am so sorry this error isn't more useful. I promise it will be soon. + +Thanks for understanding, +#### Management +"` + ); + } +} diff --git a/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js b/packages/kbn-interpreter/src/common/lib/ast/to_expression.test.js similarity index 84% rename from packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js rename to packages/kbn-interpreter/src/common/lib/ast/to_expression.test.js index 14b75ab557f01..18e6b8fe88cf1 100644 --- a/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js +++ b/packages/kbn-interpreter/src/common/lib/ast/to_expression.test.js @@ -7,8 +7,9 @@ */ import { toExpression } from '@kbn/interpreter'; +import { cloneDeep, set, unset } from 'lodash'; -describe('ast toExpression', () => { +describe('toExpression', () => { describe('single expression', () => { it('throws if no type included', () => { const errMsg = 'Objects must have a type property'; @@ -616,4 +617,110 @@ describe('ast toExpression', () => { expect(expression).toBe('both named="example" another="item" "one" "two" "three"'); }); }); + + describe('patch expression', () => { + const expression = 'f1 a=1 a=2 b=1 b={f2 c=1 c=2 | f3 d=1 d=2} | f4 e=1 e=2'; + const ast = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'f1', + arguments: { + a: [1, 2], + b: [ + 1, + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'f2', + arguments: { c: ['a', 'b'] }, + }, + { + type: 'function', + function: 'f3', + arguments: { d: [1, 2] }, + }, + ], + }, + ], + }, + }, + { + type: 'function', + function: 'f4', + arguments: { + e: [1, 2], + }, + }, + ], + }; + + it.each([ + [ + expression, + 'f1 a="updated" a=2 b=1 b={f2 c="a" c="b" | f3 d=1 d=2} | f4 e=1 e=2', + set(cloneDeep(ast), 'chain.0.arguments.a.0', 'updated'), + ], + [ + expression, + 'f1 a=1 a=2 b=1 b={f2 c="updated" c="b" | f3 d=1 d=2} | f4 e=1 e=2', + set(cloneDeep(ast), 'chain.0.arguments.b.1.chain.0.arguments.c.0', 'updated'), + ], + [ + expression, + 'f1 a={updated} a=2 b=1 b={f2 c="a" c="b" | f3 d=1 d=2} | f4 e=1 e=2', + set(cloneDeep(ast), 'chain.0.arguments.a.0', { + type: 'expression', + chain: [ + { + type: 'function', + function: 'updated', + arguments: {}, + }, + ], + }), + ], + [ + '/* comment */ f1 a /* comment */ =1', + '/* comment */ f1 a /* comment */ =2', + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'f1', + arguments: { + a: [2], + }, + }, + ], + }, + ], + ])('should patch "%s" to become "%s"', (source, expected, ast) => { + expect(toExpression(ast, { source })).toBe(expected); + }); + + it.each([ + [ + expression, + set(cloneDeep(ast), 'chain.2', { + type: 'function', + function: 'f5', + arguments: {}, + }), + ], + [expression, unset(cloneDeep(ast), 'chain.1')], + [expression, set(cloneDeep(ast), 'chain.0.function', 'updated')], + [expression, set(cloneDeep(ast), 'chain.0.arguments.c', [1])], + [expression, unset(cloneDeep(ast), 'chain.0.arguments.b')], + [expression, unset(cloneDeep(ast), 'chain.0.arguments.b.1')], + [expression, set(cloneDeep(ast), 'chain.0.arguments.b.2', 3)], + [expression, set(cloneDeep(ast), 'chain.0.arguments.b.1', 2)], + ])('should fail on patching expression', (source, ast) => { + expect(() => toExpression(ast, { source })).toThrowError(); + }); + }); }); diff --git a/packages/kbn-interpreter/src/common/lib/ast/to_expression.ts b/packages/kbn-interpreter/src/common/lib/ast/to_expression.ts new file mode 100644 index 0000000000000..fb656470ff8ee --- /dev/null +++ b/packages/kbn-interpreter/src/common/lib/ast/to_expression.ts @@ -0,0 +1,145 @@ +/* + * 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 { getType } from '../get_type'; +import type { Ast, AstArgument, AstFunction, AstNode } from './ast'; +import { isAst } from './ast'; +import { patch } from './patch'; + +interface Options { + /** + * Node type. + */ + type?: 'argument' | 'expression' | 'function'; + + /** + * Original expression to apply the new AST to. + * At the moment, only arguments values changes are supported. + */ + source?: string; +} + +function getArgumentString(arg: AstArgument, argKey?: string, level = 0): string { + const type = getType(arg); + + // eslint-disable-next-line @typescript-eslint/no-shadow + function maybeArgKey(argKey: string | null | undefined, argString: string) { + return argKey == null || argKey === '_' ? argString : `${argKey}=${argString}`; + } + + if (type === 'string') { + // correctly (re)escape double quotes + const escapedArg = (arg as string).replace(/[\\"]/g, '\\$&'); // $& means the whole matched string + return maybeArgKey(argKey, `"${escapedArg}"`); + } + + if (type === 'boolean' || type === 'null' || type === 'number') { + // use values directly + return maybeArgKey(argKey, `${arg}`); + } + + if (type === 'expression') { + // build subexpressions + return maybeArgKey(argKey, `{${getExpression((arg as Ast).chain, level + 1)}}`); + } + + // unknown type, throw with type value + throw new Error(`Invalid argument type in AST: ${type}`); +} + +function getExpressionArgs({ arguments: args }: AstFunction, level = 0) { + if (args == null || typeof args !== 'object' || Array.isArray(args)) { + throw new Error('Arguments can only be an object'); + } + + const argKeys = Object.keys(args); + const MAX_LINE_LENGTH = 80; // length before wrapping arguments + return argKeys.map((argKey) => + args[argKey].reduce((acc: string, arg) => { + const argString = getArgumentString(arg, argKey, level); + const lineLength = acc.split('\n').pop()!.length; + + // if arg values are too long, move it to the next line + if (level === 0 && lineLength + argString.length > MAX_LINE_LENGTH) { + return `${acc}\n ${argString}`; + } + + // append arg values to existing arg values + if (lineLength > 0) { + return `${acc} ${argString}`; + } + + // start the accumulator with the first arg value + return argString; + }, '') + ); +} + +function fnWithArgs(fnName: string, args: unknown[]) { + return `${fnName} ${args?.join(' ') ?? ''}`.trim(); +} + +function getExpression(chain: AstFunction[], level = 0) { + if (!chain) { + throw new Error('Expressions must contain a chain'); + } + + // break new functions onto new lines if we're not in a nested/sub-expression + const separator = level > 0 ? ' | ' : '\n| '; + + return chain + .map((item) => { + const type = getType(item); + + if (type !== 'function') { + return; + } + + const { function: fn } = item; + if (!fn) { + throw new Error('Functions must have a function name'); + } + + const expressionArgs = getExpressionArgs(item, level); + + return fnWithArgs(fn, expressionArgs); + }) + .join(separator); +} + +export function toExpression(ast: AstNode, options: string | Options = 'expression'): string { + const { type, source } = typeof options === 'string' ? ({ type: options } as Options) : options; + + if (source && isAst(ast)) { + return patch(source, ast); + } + + if (type === 'argument') { + return getArgumentString(ast as AstArgument); + } + + const nodeType = getType(ast); + + if (nodeType === 'expression') { + const { chain } = ast as Ast; + if (!Array.isArray(chain)) { + throw new Error('Expressions must contain a chain'); + } + + return getExpression(chain); + } + + if (nodeType === 'function') { + const { function: fn } = ast as AstFunction; + const args = getExpressionArgs(ast as AstFunction); + + return fnWithArgs(fn, args); + } + + throw new Error('Expression must be an expression or argument function'); +} diff --git a/packages/kbn-interpreter/src/common/lib/grammar.d.ts b/packages/kbn-interpreter/src/common/lib/grammar.d.ts new file mode 100644 index 0000000000000..e1dde5d1907e9 --- /dev/null +++ b/packages/kbn-interpreter/src/common/lib/grammar.d.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +declare module '*/grammar' { + export const parse: import('./parse').Parse; +} diff --git a/packages/kbn-interpreter/src/common/lib/parse.ts b/packages/kbn-interpreter/src/common/lib/parse.ts new file mode 100644 index 0000000000000..7570eec40c03f --- /dev/null +++ b/packages/kbn-interpreter/src/common/lib/parse.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 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 type { Ast, AstWithMeta } from './ast'; +import { parse } from '../../../grammar'; + +interface Options { + startRule?: string; +} + +interface OptionsWithMeta extends Options { + addMeta: true; +} + +export interface Parse { + (input: string, options?: Options): Ast; + (input: string, options: OptionsWithMeta): AstWithMeta; +} + +const typedParse = parse; + +export { typedParse as parse }; diff --git a/src/plugins/expressions/README.asciidoc b/src/plugins/expressions/README.asciidoc index a1462fd635d92..569c06c7e61bb 100644 --- a/src/plugins/expressions/README.asciidoc +++ b/src/plugins/expressions/README.asciidoc @@ -10,6 +10,9 @@ All the arguments to expression functions need to be serializable, as well as in Expression functions should try to stay 'pure'. This makes functions easy to reuse and also make it possible to serialize the whole chain as well as output at every step of execution. +It is possible to add comments to expressions by starting them with a `//` sequence +or by using `/*` and `*/` to enclose multi-line comments. + Expressions power visualizations in Dashboard and Lens, as well as, every *element* in Canvas is backed by an expression. @@ -30,7 +33,8 @@ filters query="SELECT COUNT(timestamp) as total_errors FROM kibana_sample_data_logs WHERE tags LIKE '%warning%' OR tags LIKE '%error%'" -| math "total_errors" +| math "total_errors" // take "total_errors" column +/* Represent as a number over a label */ | metric "TOTAL ISSUES" metricFont={font family="'Open Sans', Helvetica, Arial, sans-serif" size=48 align="left" color="#FFFFFF" weight="normal" underline=false italic=false} labelFont={font family="'Open Sans', Helvetica, Arial, sans-serif" size=30 align="left" color="#FFFFFF" weight="lighter" underline=false italic=false} diff --git a/src/plugins/expressions/common/ast/types.ts b/src/plugins/expressions/common/ast/types.ts index 8f376ac547d26..c1324245f8314 100644 --- a/src/plugins/expressions/common/ast/types.ts +++ b/src/plugins/expressions/common/ast/types.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { Ast, AstFunction } from '@kbn/interpreter'; import { ExpressionValue, ExpressionValueError } from '../expression_types'; export type ExpressionAstNode = @@ -13,14 +14,11 @@ export type ExpressionAstNode = | ExpressionAstFunction | ExpressionAstArgument; -export type ExpressionAstExpression = { - type: 'expression'; +export type ExpressionAstExpression = Omit & { chain: ExpressionAstFunction[]; }; -export type ExpressionAstFunction = { - type: 'function'; - function: string; +export type ExpressionAstFunction = Omit & { arguments: Record; /** diff --git a/src/plugins/presentation_util/public/components/expression_input/autocomplete.ts b/src/plugins/presentation_util/public/components/expression_input/autocomplete.ts index b2d486f435a47..96d991719a63b 100644 --- a/src/plugins/presentation_util/public/components/expression_input/autocomplete.ts +++ b/src/plugins/presentation_util/public/components/expression_input/autocomplete.ts @@ -7,12 +7,10 @@ */ import { uniq } from 'lodash'; -// @ts-expect-error untyped library +import type { AstWithMeta, AstArgumentWithMeta } from '@kbn/interpreter'; +import { isAstWithMeta } from '@kbn/interpreter'; import { parse } from '@kbn/interpreter'; import { - ExpressionAstExpression, - ExpressionAstFunction, - ExpressionAstArgument, ExpressionFunction, ExpressionFunctionParameter, getByAlias, @@ -40,7 +38,7 @@ interface ValueSuggestion extends BaseSuggestion { } interface FnArgAtPosition { - ast: ExpressionASTWithMeta; + ast: AstWithMeta; fnIndex: number; argName?: string; @@ -57,45 +55,6 @@ interface FnArgAtPosition { contextFn?: string | null; } -// If you parse an expression with the "addMeta" option it completely -// changes the type of returned object. The following types -// enhance the existing AST types with the appropriate meta information -interface ASTMetaInformation { - start: number; - end: number; - text: string; - node: T; -} - -// Wraps ExpressionArg with meta or replace ExpressionAstExpression with ExpressionASTWithMeta -type WrapExpressionArgWithMeta = T extends ExpressionAstExpression - ? ExpressionASTWithMeta - : ASTMetaInformation; - -type ExpressionArgASTWithMeta = WrapExpressionArgWithMeta; - -type Modify = Pick> & R; - -// Wrap ExpressionFunctionAST with meta and modify arguments to be wrapped with meta -type ExpressionFunctionASTWithMeta = Modify< - ExpressionAstFunction, - { - arguments: { - [key: string]: ExpressionArgASTWithMeta[]; - }; - } ->; - -// Wrap ExpressionFunctionAST with meta and modify chain to be wrapped with meta -type ExpressionASTWithMeta = ASTMetaInformation< - Modify< - ExpressionAstExpression, - { - chain: Array>; - } - > ->; - export interface FunctionSuggestion extends BaseSuggestion { type: 'function'; fnDef: ExpressionFunction; @@ -103,13 +62,6 @@ export interface FunctionSuggestion extends BaseSuggestion { export type AutocompleteSuggestion = FunctionSuggestion | ArgSuggestion | ValueSuggestion; -// Typeguard for checking if ExpressionArg is a new expression -function isExpression( - maybeExpression: ExpressionArgASTWithMeta -): maybeExpression is ExpressionASTWithMeta { - return typeof maybeExpression.node === 'object'; -} - /** * Generates the AST with the given expression and then returns the function and argument definitions * at the given position in the expression, if there are any. @@ -120,9 +72,9 @@ export function getFnArgDefAtPosition( position: number ) { try { - const ast: ExpressionASTWithMeta = parse(expression, { + const ast: AstWithMeta = parse(expression, { addMeta: true, - }) as ExpressionASTWithMeta; + }) as AstWithMeta; const { ast: newAst, fnIndex, argName, argStart, argEnd } = getFnArgAtPosition(ast, position); const fn = newAst.node.chain[fnIndex].node; @@ -153,7 +105,7 @@ export function getAutocompleteSuggestions( ): AutocompleteSuggestion[] { const text = expression.substr(0, position) + MARKER + expression.substr(position); try { - const ast = parse(text, { addMeta: true }) as ExpressionASTWithMeta; + const ast = parse(text, { addMeta: true }) as AstWithMeta; const { ast: newAst, fnIndex, @@ -206,7 +158,7 @@ export function getAutocompleteSuggestions( The context function for the first expression in the chain is `math`, since it's the parent's previous item. The context function for `formatnumber` is the return of `math "divide(value, 2)"`. */ -function getFnArgAtPosition(ast: ExpressionASTWithMeta, position: number): FnArgAtPosition { +function getFnArgAtPosition(ast: AstWithMeta, position: number): FnArgAtPosition { const fnIndex = ast.node.chain.findIndex((fn) => fn.start <= position && position <= fn.end); const fn = ast.node.chain[fnIndex]; for (const [argName, argValues] of Object.entries(fn.node.arguments)) { @@ -222,7 +174,7 @@ function getFnArgAtPosition(ast: ExpressionASTWithMeta, position: number): FnArg // If the arg value is an expression, expand our start and end position // to include the opening and closing braces - if (value.node !== null && isExpression(value)) { + if (value.node !== null && isAstWithMeta(value)) { argStart--; argEnd++; } @@ -233,7 +185,7 @@ function getFnArgAtPosition(ast: ExpressionASTWithMeta, position: number): FnArg // argument name (`font=` for example), recurse within the expression if ( value.node !== null && - isExpression(value) && + isAstWithMeta(value) && (argName === '_' || !(argStart <= position && position <= argStart + argName.length + 1)) ) { const result = getFnArgAtPosition(value, position); @@ -265,7 +217,7 @@ function getFnArgAtPosition(ast: ExpressionASTWithMeta, position: number): FnArg function getFnNameSuggestions( specs: ExpressionFunction[], - ast: ExpressionASTWithMeta, + ast: AstWithMeta, fnIndex: number ): FunctionSuggestion[] { // Filter the list of functions by the text at the marker @@ -299,7 +251,7 @@ function getFnNameSuggestions( function getSubFnNameSuggestions( specs: ExpressionFunction[], - ast: ExpressionASTWithMeta, + ast: AstWithMeta, fnIndex: number, parentFn: string, parentFnArgName: string, @@ -391,7 +343,7 @@ function getScore( function getArgNameSuggestions( specs: ExpressionFunction[], - ast: ExpressionASTWithMeta, + ast: AstWithMeta, fnIndex: number, argName: string, argIndex: number @@ -407,7 +359,7 @@ function getArgNameSuggestions( const { start, end } = fn.arguments[argName][argIndex]; // Filter the list of args by those which aren't already present (unless they allow multi) - const argEntries = Object.entries(fn.arguments).map<[string, ExpressionArgASTWithMeta[]]>( + const argEntries = Object.entries(fn.arguments).map<[string, AstArgumentWithMeta[]]>( ([name, values]) => { return [name, values.filter((value) => !value.text.includes(MARKER))]; } @@ -442,7 +394,7 @@ function getArgNameSuggestions( function getArgValueSuggestions( specs: ExpressionFunction[], - ast: ExpressionASTWithMeta, + ast: AstWithMeta, fnIndex: number, argName: string, argIndex: number diff --git a/src/plugins/presentation_util/public/components/expression_input/language.ts b/src/plugins/presentation_util/public/components/expression_input/language.ts index a481e5afed24e..6dc3c252ebb03 100644 --- a/src/plugins/presentation_util/public/components/expression_input/language.ts +++ b/src/plugins/presentation_util/public/components/expression_input/language.ts @@ -72,6 +72,9 @@ const expressionsLanguage: ExpressionsLanguage = { [/'/, 'string', '@string_single'], [/@symbols/, 'delimiter'], + + [/\/\*/, 'comment', '@multiline_comment'], + [/\/\/.*$/, 'comment'], ], string_double: [ @@ -93,6 +96,12 @@ const expressionsLanguage: ExpressionsLanguage = { [/\}/, 'delimiter.bracket', '@pop'], { include: 'common' }, ], + + multiline_comment: [ + [/[^\/*]+/, 'comment'], + ['\\*/', 'comment', '@pop'], + [/[\/*]/, 'comment'], + ], }, }; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts b/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts index 21bcc89304b3c..bdccc8040c5de 100644 --- a/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts +++ b/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ExpressionFunctionAST, fromExpression } from '@kbn/interpreter'; +import { AstFunction, fromExpression } from '@kbn/interpreter'; import { shallowEqual, useSelector } from 'react-redux'; import { State } from '../../../../types'; import { getFiltersByFilterExpressions } from '../../../lib/filter'; @@ -14,7 +14,7 @@ import { useFiltersService } from '../../../services'; const extractExpressionAST = (filters: string[]) => fromExpression(filters.join(' | ')); -export function useCanvasFilters(filterExprsToGroupBy: ExpressionFunctionAST[] = []) { +export function useCanvasFilters(filterExprsToGroupBy: AstFunction[] = []) { const filtersService = useFiltersService(); const filterExpressions = useSelector( (state: State) => filtersService.getFilters(state), diff --git a/x-pack/plugins/canvas/public/lib/filter.ts b/x-pack/plugins/canvas/public/lib/filter.ts index 2554ae11220eb..dceb7f15b1717 100644 --- a/x-pack/plugins/canvas/public/lib/filter.ts +++ b/x-pack/plugins/canvas/public/lib/filter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Ast, ExpressionFunctionAST, fromExpression, toExpression } from '@kbn/interpreter'; +import { Ast, AstFunction, fromExpression, toExpression } from '@kbn/interpreter'; import { flowRight, get, groupBy } from 'lodash'; import { Filter as FilterType, @@ -63,7 +63,7 @@ export const groupFiltersBy = (filters: FilterType[], groupByField: FilterField) })); }; -const excludeFiltersByGroups = (filters: Ast[], filterExprAst: ExpressionFunctionAST) => { +const excludeFiltersByGroups = (filters: Ast[], filterExprAst: AstFunction) => { const groupsToExclude = filterExprAst.arguments.group ?? []; const removeUngrouped = filterExprAst.arguments.ungrouped?.[0] ?? false; return filters.filter((filter) => { @@ -85,7 +85,7 @@ const excludeFiltersByGroups = (filters: Ast[], filterExprAst: ExpressionFunctio const includeFiltersByGroups = ( filters: Ast[], - filterExprAst: ExpressionFunctionAST, + filterExprAst: AstFunction, ignoreUngroupedIfGroups: boolean = false ) => { const groupsToInclude = filterExprAst.arguments.group ?? []; @@ -109,7 +109,7 @@ const includeFiltersByGroups = ( export const getFiltersByFilterExpressions = ( filters: string[], - filterExprsAsts: ExpressionFunctionAST[] + filterExprsAsts: AstFunction[] ) => { const filtersAst = filters.map((filter) => fromExpression(filter)); const matchedFiltersAst = filterExprsAsts.reduce((includedFilters, filter) => { diff --git a/x-pack/plugins/canvas/public/lib/filter_adapters.test.ts b/x-pack/plugins/canvas/public/lib/filter_adapters.test.ts index e035e30e307f0..33e675a6b9d98 100644 --- a/x-pack/plugins/canvas/public/lib/filter_adapters.test.ts +++ b/x-pack/plugins/canvas/public/lib/filter_adapters.test.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { ExpressionFunctionAST } from '@kbn/interpreter'; +import { AstFunction } from '@kbn/interpreter'; import { adaptCanvasFilter } from './filter_adapters'; describe('adaptCanvasFilter', () => { - const filterAST: ExpressionFunctionAST = { + const filterAST: AstFunction = { type: 'function', function: 'exactly', arguments: { diff --git a/x-pack/plugins/canvas/public/lib/filter_adapters.ts b/x-pack/plugins/canvas/public/lib/filter_adapters.ts index 67bdf13d19999..ed303a83d5e5d 100644 --- a/x-pack/plugins/canvas/public/lib/filter_adapters.ts +++ b/x-pack/plugins/canvas/public/lib/filter_adapters.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ExpressionFunctionAST } from '@kbn/interpreter'; +import type { AstFunction } from '@kbn/interpreter'; import { identity } from 'lodash'; import { ExpressionAstArgument, Filter, FilterType } from '../../types'; @@ -23,7 +23,7 @@ const argToValue = ( const convertFunctionToFilterType = (func: string) => functionToFilter[func] ?? FilterType.exactly; -const collectArgs = (args: ExpressionFunctionAST['arguments']) => { +const collectArgs = (args: AstFunction['arguments']) => { const argsKeys = Object.keys(args); if (!argsKeys.length) { @@ -36,7 +36,7 @@ const collectArgs = (args: ExpressionFunctionAST['arguments']) => { ); }; -export function adaptCanvasFilter(filter: ExpressionFunctionAST): Filter { +export function adaptCanvasFilter(filter: AstFunction): Filter { const { function: type, arguments: args } = filter; const { column, filterGroup, value: valueArg, type: typeArg, ...rest } = args ?? {}; return { diff --git a/x-pack/plugins/canvas/server/collectors/collector_helpers.ts b/x-pack/plugins/canvas/server/collectors/collector_helpers.ts index 2a1dbdfe437ba..aa88d910098a5 100644 --- a/x-pack/plugins/canvas/server/collectors/collector_helpers.ts +++ b/x-pack/plugins/canvas/server/collectors/collector_helpers.ts @@ -10,19 +10,13 @@ * @param cb: callback to do something with a function that has been found */ -import { - ExpressionAstExpression, - ExpressionAstNode, -} from '../../../../../src/plugins/expressions/common'; - -function isExpression( - maybeExpression: ExpressionAstNode -): maybeExpression is ExpressionAstExpression { - return typeof maybeExpression === 'object' && maybeExpression.type === 'expression'; -} +import { isAst } from '@kbn/interpreter'; +import { ExpressionAstNode } from '../../../../../src/plugins/expressions/common'; export function collectFns(ast: ExpressionAstNode, cb: (functionName: string) => void) { - if (!isExpression(ast)) return; + if (!isAst(ast)) { + return; + } ast.chain.forEach(({ function: cFunction, arguments: cArguments }) => { cb(cFunction); diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts b/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts index 806f08b17651c..6d76dc61bd96b 100644 --- a/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts +++ b/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts @@ -31,7 +31,10 @@ export const extractReferences = ( })) ); - return { ...element, expression: toExpression(extract.state) }; + return { + ...element, + expression: toExpression(extract.state, { source: element.expression }), + }; }); return { ...page, elements }; @@ -59,7 +62,7 @@ export const injectReferences = ( referencesForElement ); - return { ...element, expression: toExpression(injectedAst) }; + return { ...element, expression: toExpression(injectedAst, { source: element.expression }) }; }); return { ...page, elements }; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts index a8d58e51691f9..600341931f575 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter'; +import { Ast, AstFunction, fromExpression } from '@kbn/interpreter'; import { DatasourceStates } from '../../state_management'; import { Visualization, DatasourcePublicAPI, DatasourceMap } from '../../types'; @@ -35,7 +35,7 @@ export function prependDatasourceExpression( ([layerId, expr]) => [layerId, typeof expr === 'string' ? fromExpression(expr) : expr] ); - const datafetchExpression: ExpressionFunctionAST = { + const datafetchExpression: AstFunction = { type: 'function', function: 'lens_merge_tables', arguments: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts index cec6294f7f314..5bae2f5a1865f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import type { ExpressionFunctionAST } from '@kbn/interpreter'; +import type { AstFunction } from '@kbn/interpreter'; import memoizeOne from 'memoize-one'; import { LayerType, layerTypes } from '../../../../../common'; import type { TimeScaleUnit } from '../../../../../common/expressions'; @@ -143,7 +143,7 @@ export function dateBasedOperationToExpression( columnId: string, functionName: string, additionalArgs: Record = {} -): ExpressionFunctionAST[] { +): AstFunction[] { const currentColumn = layer.columns[columnId] as unknown as ReferenceBasedIndexPatternColumn; const buckets = layer.columnOrder.filter((colId) => layer.columns[colId].isBucketed); const dateColumnIndex = buckets.findIndex( @@ -174,7 +174,7 @@ export function optionallHistogramBasedOperationToExpression( columnId: string, functionName: string, additionalArgs: Record = {} -): ExpressionFunctionAST[] { +): AstFunction[] { const currentColumn = layer.columns[columnId] as unknown as ReferenceBasedIndexPatternColumn; const buckets = layer.columnOrder.filter((colId) => layer.columns[colId].isBucketed); const nonHistogramColumns = buckets.filter( diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index 596ddf12ae7c3..fc1718c1df2fa 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -6,7 +6,7 @@ */ import { cloneDeep, mergeWith } from 'lodash'; -import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter'; +import { fromExpression, toExpression, Ast, AstFunction } from '@kbn/interpreter'; import { SavedObjectMigrationMap, SavedObjectMigrationFn, @@ -141,7 +141,7 @@ const removeLensAutoDate: SavedObjectMigrationFn { + const newChain: AstFunction[] = ast.chain.map((topNode) => { if (topNode.function !== 'lens_merge_tables') { return topNode; } @@ -202,7 +202,7 @@ const addTimeFieldToEsaggs: SavedObjectMigrationFn { + const newChain: AstFunction[] = ast.chain.map((topNode) => { if (topNode.function !== 'lens_merge_tables') { return topNode; } From 35f721db9bef5ffd0d2787a9da83a568c9c598ca Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 20 Jan 2022 20:00:10 +0000 Subject: [PATCH 19/43] skip flaky suite (#123495) --- .../endpoint/validators/base_validator.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts index 728e3b8559ed3..9d19ed4178a2a 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts @@ -21,7 +21,8 @@ import { GLOBAL_ARTIFACT_TAG, } from '../../../../common/endpoint/service/artifacts'; -describe('When using Artifacts Exceptions BaseValidator', () => { +// FLAKY: https://github.com/elastic/kibana/issues/123495 +describe.skip('When using Artifacts Exceptions BaseValidator', () => { let endpointAppContextServices: EndpointAppContextService; let kibanaRequest: ReturnType; let exceptionLikeItem: ExceptionItemLikeOptions; From 67706ca03312c657f3baeb6bc58ae79f8f776a9d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 20 Jan 2022 20:03:25 +0000 Subject: [PATCH 20/43] skip flaky suite (#123498) --- .../apis/endpoint_artifacts.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts index 58f6fbf58ccb5..1e6646b4fbb24 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts @@ -210,7 +210,8 @@ export default function ({ getService }: FtrProviderContext) { } }); - describe('and user DOES NOT have authorization to manage endpoint security', () => { + // FLAKY: https://github.com/elastic/kibana/issues/123498 + describe.skip('and user DOES NOT have authorization to manage endpoint security', () => { for (const trustedAppApiCall of trustedAppApiCalls) { it(`should error on [${trustedAppApiCall.method}]`, async () => { await supertestWithoutAuth[trustedAppApiCall.method](trustedAppApiCall.path) From 7740a5db127ff321bc0e364d8314b9fe2cbcfd22 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 20 Jan 2022 15:08:14 -0500 Subject: [PATCH 21/43] Revert "skip flaky suite (#123498)" This reverts commit 67706ca03312c657f3baeb6bc58ae79f8f776a9d. --- .../apis/endpoint_artifacts.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts index 1e6646b4fbb24..58f6fbf58ccb5 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts @@ -210,8 +210,7 @@ export default function ({ getService }: FtrProviderContext) { } }); - // FLAKY: https://github.com/elastic/kibana/issues/123498 - describe.skip('and user DOES NOT have authorization to manage endpoint security', () => { + describe('and user DOES NOT have authorization to manage endpoint security', () => { for (const trustedAppApiCall of trustedAppApiCalls) { it(`should error on [${trustedAppApiCall.method}]`, async () => { await supertestWithoutAuth[trustedAppApiCall.method](trustedAppApiCall.path) From ba0833f42b89a0b62bccd1d8c04bf9c799f32258 Mon Sep 17 00:00:00 2001 From: Georgii Gorbachev Date: Fri, 21 Jan 2022 00:17:26 +0300 Subject: [PATCH 22/43] [Security Solution][Detections] Rule execution logging overhaul (#121644) **Epic:** https://github.com/elastic/kibana/issues/118324 **Tickets:** https://github.com/elastic/kibana/issues/119603, https://github.com/elastic/kibana/issues/119597, https://github.com/elastic/kibana/issues/91265, https://github.com/elastic/kibana/issues/118511 ## Summary The legacy rule execution logging implementation is replaced by a new one that introduces a new model for execution-related data, a new saved object and a new, cleaner interface and implementation. - [x] The legacy data model is deleted (`IRuleStatusResponseAttributes`, `IRuleStatusSOAttributes`) - [x] The legacy `siem-detection-engine-rule-status` saved object type is deleted and marked as deleted in `src/core` - [x] A new data model is introduced (`x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts`). This data model doesn't contain a mixture of successful and failed statuses, which should simplify client-side code (e.g. the code of Rule Management and Monitoring tables, as well as Rule Details page). - [x] A new `siem-detection-engine-rule-execution-info` saved object is introduced (`x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_info/saved_object.ts`). - [x] This SO has 1:1 association with the rule SO, so every rule can have 0 or 1 execution info associated with it. This SO is used in order to 1) update the last execution status and metrics and 2) fetch execution data for N rules more efficiently comparing to the legacy SO. - [x] The logic of creating or updating this SOs is based on the "upsert" approach (planned in https://github.com/elastic/kibana/issues/118511). It does not fetch the SO by rule id before updating it anymore. - [x] Rule execution logging logic is rewritten (see `x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log`). The previous rule execution log client is split into two objects: `IRuleExecutionLogClient` for using it from route handlers, and `IRuleExecutionLogger` for writing logs from rule executors. - [x] `IRuleExecutionLogger` instance is scoped to the currently executing rule and space id. There's no need to pass rule id, name, type etc to `.logStatusChange()` every time. - [x] Rule executors and related functions are updated. - [x] API routes are updated, including the rule preview route which uses a special "spy" implementation of `IRuleExecutionLogger`. A rule returned from an API endpoint now has optional `execution_summary` field of type `RuleExecutionSummary`. - [x] UI is updated to use the new data model of `RuleExecutionSummary`: - [x] Rule Management and Monitoring tables - [x] Rule Details page - [x] A new API route is introduced for fetching rule execution events: `/internal/detection_engine/rules/{ruleId}/execution/events`. It is used for rendering the Failure History tab (last 5 failures) and is intended to be used in the coming UI of Rule Execution Log on the Details page. - [x] Rule Details page and Failure History tab are updated to use the new data models and API routes. - [x] I used `react-query` for fetching execution events - [x] See `x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx` - [x] The lib is updated to the latest version - [x] Tests and fixed and updated according to all the changes - [x] Components related to rule execution statuses are all moved to `x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status`. - [x] I left a lot of `// TODO: https://github.com/elastic/kibana/pull/121644` comments in the code which I'm planning to address and remove in a follow-up PR. Lots of clean up work is needed, but I'd like to unblock the work on Rule Execution Log UI. ## In the next episodes - Address and remove `// TODO: https://github.com/elastic/kibana/pull/121644` comments in the code - Make sure that SO id generation for `siem-detection-engine-rule-execution-info` is safe and future-proof. Sync with the Core team. If there are risks, we will need to choose between risks and performance (reading the SO before updating it). It would be easy to submit a fix if needed. - Add APM integration. Use `withSecuritySpan` in methods of `rule_execution_log` citizens. - Add comments to the code and README. - Add test coverage. - Etc... ### Checklist Delete any items that are not applicable to this PR. - [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 - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- package.json | 2 +- .../transform_data_to_ndjson/index.test.ts | 4 - ...grations_state_action_machine.test.ts.snap | 30 +++ .../migrations/core/unused_types.ts | 2 + .../type_registrations.test.ts | 1 + .../reporting_example/public/application.tsx | 2 +- .../security_solution/common/constants.ts | 6 +- .../detection_engine/schemas/common/index.ts} | 9 +- .../schemas/common/rule_monitoring.ts | 84 +++++++ .../schemas/common/schemas.ts | 37 --- .../request/find_rule_statuses_schema.ts | 28 --- .../get_rule_execution_events_request.ts | 18 ++ .../schemas/request/rule_schemas.ts | 23 +- .../get_rule_execution_events_response.ts | 17 ++ .../schemas/response/index.ts | 1 + .../schemas/response/rules_schema.mocks.ts | 15 +- .../schemas/response/rules_schema.ts | 16 +- .../common/detection_engine/utils.ts | 21 +- .../rules/rule_execution_status/index.tsx | 13 ++ .../rule_status.tsx} | 26 ++- .../rule_status_badge.test.tsx} | 11 +- .../rule_status_badge.tsx} | 23 +- .../rule_status_failed_callout.test.tsx | 57 +++++ .../rule_status_failed_callout.tsx | 81 +++++++ .../translations.ts | 14 ++ .../rule_execution_status/utils.test.tsx | 16 ++ .../utils.ts} | 19 +- .../detection_engine/rules/__mocks__/api.ts | 84 +++---- .../detection_engine/rules/api.test.ts | 45 ++-- .../containers/detection_engine/rules/api.ts | 55 +++-- .../detection_engine/rules/index.ts | 2 +- .../detection_engine/rules/types.ts | 36 +-- .../rules/use_rule_execution_events.test.tsx | 57 +++++ .../rules/use_rule_execution_events.tsx | 31 +++ .../rules/use_rule_status.test.tsx | 85 ------- .../rules/use_rule_status.tsx | 67 ------ .../detection_engine/rules/all/columns.tsx | 51 ++-- .../rules/details/failure_history.test.tsx | 88 ------- .../rules/details/failure_history.tsx | 78 +++---- .../rules/details/index.test.tsx | 10 - .../detection_engine/rules/details/index.tsx | 87 +++---- .../details/status_failed_callout.test.tsx | 26 --- .../rules/details/status_failed_callout.tsx | 43 ---- .../rules/details/translations.ts | 14 -- .../security_solution/server/config.mock.ts | 4 - .../security_solution/server/config.ts | 14 -- .../plugins/security_solution/server/index.ts | 3 +- .../routes/__mocks__/request_context.ts | 4 +- .../routes/__mocks__/request_responses.ts | 145 ++++++------ .../routes/__mocks__/utils.ts | 7 +- .../rules/add_prepackaged_rules_route.test.ts | 25 +- .../rules/add_prepackaged_rules_route.ts | 9 +- .../rules/create_rules_bulk_route.test.ts | 17 +- .../routes/rules/create_rules_bulk_route.ts | 10 +- .../routes/rules/create_rules_route.test.ts | 25 +- .../routes/rules/create_rules_route.ts | 20 +- .../rules/delete_rules_bulk_route.test.ts | 7 - .../routes/rules/delete_rules_bulk_route.ts | 23 +- .../routes/rules/delete_rules_route.test.ts | 12 - .../routes/rules/delete_rules_route.ts | 19 +- .../routes/rules/export_rules_route.ts | 6 +- .../find_rule_status_internal_route.test.ts | 119 ---------- .../rules/find_rule_status_internal_route.ts | 85 ------- .../routes/rules/find_rules_route.test.ts | 19 +- .../routes/rules/find_rules_route.ts | 19 +- ...get_prepackaged_rules_status_route.test.ts | 11 +- .../get_prepackaged_rules_status_route.ts | 8 +- .../get_rule_execution_events_route.test.ts | 53 +++++ .../rules/get_rule_execution_events_route.ts | 58 +++++ .../routes/rules/import_rules_route.ts | 2 - .../rules/patch_rules_bulk_route.test.ts | 11 +- .../routes/rules/patch_rules_bulk_route.ts | 22 +- .../routes/rules/patch_rules_route.test.ts | 20 +- .../routes/rules/patch_rules_route.ts | 19 +- .../rules/perform_bulk_action_route.test.ts | 7 - .../routes/rules/perform_bulk_action_route.ts | 10 +- .../routes/rules/preview_rules_route.ts | 45 ++-- .../routes/rules/read_rules_route.test.ts | 17 +- .../routes/rules/read_rules_route.ts | 18 +- .../rules/update_rules_bulk_route.test.ts | 19 +- .../routes/rules/update_rules_bulk_route.ts | 24 +- .../routes/rules/update_rules_route.test.ts | 26 +-- .../routes/rules/update_rules_route.ts | 22 +- .../detection_engine/routes/rules/utils.ts | 47 ++-- .../rules/utils/get_current_rule_statuses.ts | 64 ----- .../rules/utils/import_rules_utils.test.ts | 8 - .../routes/rules/utils/import_rules_utils.ts | 4 - .../routes/rules/validate.test.ts | 28 ++- .../detection_engine/routes/rules/validate.ts | 68 +++--- .../lib/detection_engine/routes/utils.test.ts | 165 +------------ .../lib/detection_engine/routes/utils.ts | 68 +----- .../__mocks__/rule_execution_log_client.ts | 34 ++- .../event_log_adapter/event_log_adapter.ts | 126 ---------- .../event_log_adapter/event_log_client.ts | 221 ------------------ .../rule_execution_log/index.ts | 14 +- .../merge_rule_execution_summary.ts | 43 ++++ .../constants.ts | 4 +- .../rule_execution_events/events_reader.ts | 86 +++++++ .../rule_execution_events/events_writer.ts | 126 ++++++++++ .../register_event_log_provider.ts | 0 .../rule_execution_info/saved_object.ts | 77 ++++++ .../saved_objects_client.ts | 177 ++++++++++++++ .../saved_objects_utils.ts | 36 +++ .../rule_execution_log_client.ts | 131 ----------- .../rule_execution_log_client/client.ts | 47 ++++ .../client_interface.ts | 45 ++++ .../rule_execution_log_client/decorator.ts | 64 +++++ .../rule_execution_log_factory.ts | 58 +++++ .../rule_execution_logger/logger.ts | 145 ++++++++++++ .../rule_execution_logger/logger_interface.ts | 48 ++++ .../rule_status_saved_objects_client.ts | 139 ----------- .../saved_objects_adapter.ts | 191 --------------- .../rule_execution_log/types.ts | 120 ---------- .../rule_execution_log/utils/logging.ts | 24 ++ .../rule_execution_log/utils/normalization.ts | 26 ++- .../rule_types/__mocks__/threshold.ts | 5 +- .../create_security_rule_type_wrapper.ts | 77 +++--- .../query/create_query_alert_type.test.ts | 4 +- .../lib/detection_engine/rule_types/types.ts | 4 +- .../rules/delete_rules.test.ts | 10 +- .../detection_engine/rules/delete_rules.ts | 8 +- .../rules/get_export_by_object_ids.test.ts | 7 +- .../rules/get_export_by_object_ids.ts | 2 +- .../legacy_rule_status/legacy_migrations.ts | 110 --------- ...egacy_rule_status_saved_object_mappings.ts | 74 ------ .../rules/legacy_rule_status/legacy_utils.ts | 17 -- .../lib/detection_engine/rules/types.ts | 69 +----- .../rules/update_prepacked_rules.test.ts | 6 +- .../rules/update_prepacked_rules.ts | 6 - .../rules/update_rules.mock.ts | 3 - .../schemas/rule_converters.ts | 39 +--- ...s.sh => delete_all_rule_execution_data.sh} | 20 +- .../detection_engine/scripts/hard_reset.sh | 9 +- .../signals/__mocks__/es_results.ts | 38 --- .../signals/build_signal.test.ts | 9 - .../preview_rule_execution_log_client.ts | 78 ------- .../preview/preview_rule_execution_logger.ts | 52 +++++ .../detection_engine/signals/utils.test.ts | 79 ++++--- .../lib/detection_engine/signals/utils.ts | 61 ++--- .../security_solution/server/plugin.ts | 6 +- .../server/request_context_factory.ts | 19 +- .../security_solution/server/routes/index.ts | 8 +- .../security_solution/server/saved_objects.ts | 7 +- .../plugins/security_solution/server/types.ts | 2 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../tests/check_privileges.ts | 8 +- .../security_and_spaces/tests/create_rules.ts | 13 +- .../tests/create_threat_matching.ts | 8 +- .../tests/generating_signals.ts | 5 +- .../security_and_spaces/tests/migrations.ts | 89 ------- .../detection_engine_api_integration/utils.ts | 30 ++- .../security_solution/migrations/data.json | 63 ----- .../migrations/mappings.json | 38 --- yarn.lock | 8 +- 155 files changed, 2386 insertions(+), 3562 deletions(-) rename x-pack/plugins/security_solution/{public/detections/components/rules/rule_status/helpers.test.tsx => common/detection_engine/schemas/common/index.ts} (54%) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rule_statuses_schema.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_rule_execution_events_request.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/response/get_rule_execution_events_response.ts create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/index.tsx rename x-pack/plugins/security_solution/public/detections/components/rules/{rule_status/index.tsx => rule_execution_status/rule_status.tsx} (62%) rename x-pack/plugins/security_solution/public/detections/components/rules/{rule_execution_status_badge/index.test.tsx => rule_execution_status/rule_status_badge.test.tsx} (61%) rename x-pack/plugins/security_solution/public/detections/components/rules/{rule_execution_status_badge/index.tsx => rule_execution_status/rule_status_badge.tsx} (53%) create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx rename x-pack/plugins/security_solution/public/detections/components/rules/{rule_status => rule_execution_status}/translations.ts (71%) create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.test.tsx rename x-pack/plugins/security_solution/public/detections/components/rules/{rule_status/helpers.ts => rule_execution_status/utils.ts} (55%) create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_status_internal_route.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_status_internal_route.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/get_current_rule_statuses.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_client.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/merge_rule_execution_summary.ts rename x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/{event_log_adapter => rule_execution_events}/constants.ts (90%) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/events_reader.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/events_writer.ts rename x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/{event_log_adapter => rule_execution_events}/register_event_log_provider.ts (100%) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_info/saved_object.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_info/saved_objects_client.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_info/saved_objects_utils.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client/client.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client/client_interface.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client/decorator.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_factory.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger_interface.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/utils/logging.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_utils.ts rename x-pack/plugins/security_solution/server/lib/detection_engine/scripts/{delete_all_statuses.sh => delete_all_rule_execution_data.sh} (52%) delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_log_client.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_logger.ts diff --git a/package.json b/package.json index 429d1ea67e73f..0bd1b5c9e5b55 100644 --- a/package.json +++ b/package.json @@ -340,7 +340,7 @@ "react-moment-proptypes": "^1.7.0", "react-monaco-editor": "^0.41.2", "react-popper-tooltip": "^2.10.1", - "react-query": "^3.28.0", + "react-query": "^3.34.0", "react-redux": "^7.2.0", "react-resizable": "^1.7.5", "react-resize-detector": "^4.2.0", diff --git a/packages/kbn-securitysolution-utils/src/transform_data_to_ndjson/index.test.ts b/packages/kbn-securitysolution-utils/src/transform_data_to_ndjson/index.test.ts index b10626357f5b1..bb4fc47ce0370 100644 --- a/packages/kbn-securitysolution-utils/src/transform_data_to_ndjson/index.test.ts +++ b/packages/kbn-securitysolution-utils/src/transform_data_to_ndjson/index.test.ts @@ -32,10 +32,6 @@ const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE) => ({ type: 'query', threat: [], version: 1, - status: 'succeeded', - status_date: '2020-02-22T16:47:50.047Z', - last_success_at: '2020-02-22T16:47:50.047Z', - last_success_message: 'succeeded', output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, diff --git a/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap index 760a83fa65cf1..0ad6f18104685 100644 --- a/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap +++ b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap @@ -119,6 +119,11 @@ Object { "type": "server", }, }, + Object { + "term": Object { + "type": "siem-detection-engine-rule-status", + }, + }, Object { "term": Object { "type": "timelion-sheet", @@ -275,6 +280,11 @@ Object { "type": "server", }, }, + Object { + "term": Object { + "type": "siem-detection-engine-rule-status", + }, + }, Object { "term": Object { "type": "timelion-sheet", @@ -435,6 +445,11 @@ Object { "type": "server", }, }, + Object { + "term": Object { + "type": "siem-detection-engine-rule-status", + }, + }, Object { "term": Object { "type": "timelion-sheet", @@ -599,6 +614,11 @@ Object { "type": "server", }, }, + Object { + "term": Object { + "type": "siem-detection-engine-rule-status", + }, + }, Object { "term": Object { "type": "timelion-sheet", @@ -800,6 +820,11 @@ Object { "type": "server", }, }, + Object { + "term": Object { + "type": "siem-detection-engine-rule-status", + }, + }, Object { "term": Object { "type": "timelion-sheet", @@ -967,6 +992,11 @@ Object { "type": "server", }, }, + Object { + "term": Object { + "type": "siem-detection-engine-rule-status", + }, + }, Object { "term": Object { "type": "timelion-sheet", diff --git a/src/core/server/saved_objects/migrations/core/unused_types.ts b/src/core/server/saved_objects/migrations/core/unused_types.ts index ddcadc09502f4..fd4b8a09600d7 100644 --- a/src/core/server/saved_objects/migrations/core/unused_types.ts +++ b/src/core/server/saved_objects/migrations/core/unused_types.ts @@ -29,6 +29,8 @@ export const REMOVED_TYPES: string[] = [ 'tsvb-validation-telemetry', // replaced by osquery-manager-usage-metric 'osquery-usage-metric', + // Was removed in 8.1 https://github.com/elastic/kibana/issues/91265 + 'siem-detection-engine-rule-status', // Was removed in 7.16 'timelion-sheet', ].sort(); diff --git a/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts b/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts index 4ff66151db925..962d32b44eb32 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts @@ -84,6 +84,7 @@ const previouslyRegisteredTypes = [ 'security-solution-signals-migration', 'server', 'siem-detection-engine-rule-actions', + 'siem-detection-engine-rule-execution-info', 'siem-detection-engine-rule-status', 'siem-ui-timeline', 'siem-ui-timeline-note', diff --git a/x-pack/examples/reporting_example/public/application.tsx b/x-pack/examples/reporting_example/public/application.tsx index 9b044ac801773..c1e18b13ebe5e 100644 --- a/x-pack/examples/reporting_example/public/application.tsx +++ b/x-pack/examples/reporting_example/public/application.tsx @@ -9,7 +9,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Router, Route, Switch } from 'react-router-dom'; import { AppMountParameters, CoreStart } from '../../../../src/core/public'; -import { KibanaThemeProvider } from '../../../../../kibana/src/plugins/kibana_react/public'; +import { KibanaThemeProvider } from '../../../../src/plugins/kibana_react/public'; import { CaptureTest } from './containers/capture_test'; import { Main } from './containers/main'; import { ApplicationContextProvider } from './application_context'; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 7514edea6247f..6e408bfb0822a 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -264,8 +264,10 @@ export const DETECTION_ENGINE_RULES_PREVIEW = `${DETECTION_ENGINE_RULES_URL}/pre * Internal detection engine routes */ export const INTERNAL_DETECTION_ENGINE_URL = '/internal/detection_engine' as const; -export const INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL = - `${INTERNAL_DETECTION_ENGINE_URL}/rules/_find_status` as const; +export const DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL = + `${INTERNAL_DETECTION_ENGINE_URL}/rules/{ruleId}/execution/events` as const; +export const detectionEngineRuleExecutionEventsUrl = (ruleId: string) => + `${INTERNAL_DETECTION_ENGINE_URL}/rules/${ruleId}/execution/events` as const; export const TIMELINE_RESOLVE_URL = '/api/timeline/resolve' as const; export const TIMELINE_URL = '/api/timeline' as const; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.test.tsx b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts similarity index 54% rename from x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.test.tsx rename to x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts index bfc972d75fcc3..4ef5d6178d5a5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.test.tsx +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts @@ -5,10 +5,5 @@ * 2.0. */ -import { getStatusColor } from './helpers'; - -describe('rule_status helpers', () => { - it('getStatusColor returns subdued if null was provided', () => { - expect(getStatusColor(null)).toBe('subdued'); - }); -}); +export * from './rule_monitoring'; +export * from './schemas'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts new file mode 100644 index 0000000000000..afc6fad33a4bd --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts @@ -0,0 +1,84 @@ +/* + * 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 { enumeration, IsoDateString, PositiveInteger } from '@kbn/securitysolution-io-ts-types'; + +// ------------------------------------------------------------------------------------------------- +// Rule execution status + +/** + * Custom execution status of Security rules that is different from the status + * used in the Alerting Framework. We merge our custom status with the + * Framework's status to determine the resulting status of a rule. + */ +export enum RuleExecutionStatus { + 'succeeded' = 'succeeded', + 'failed' = 'failed', + 'going to run' = 'going to run', + 'partial failure' = 'partial failure', + /** + * @deprecated 'partial failure' status should be used instead + */ + 'warning' = 'warning', +} + +export const ruleExecutionStatus = enumeration('RuleExecutionStatus', RuleExecutionStatus); + +export const ruleExecutionStatusOrder = PositiveInteger; +export type RuleExecutionStatusOrder = t.TypeOf; + +export const ruleExecutionStatusOrderByStatus: Record< + RuleExecutionStatus, + RuleExecutionStatusOrder +> = { + [RuleExecutionStatus.succeeded]: 0, + [RuleExecutionStatus['going to run']]: 10, + [RuleExecutionStatus.warning]: 20, + [RuleExecutionStatus['partial failure']]: 20, + [RuleExecutionStatus.failed]: 30, +}; + +// ------------------------------------------------------------------------------------------------- +// Rule execution metrics + +export const durationMetric = PositiveInteger; +export type DurationMetric = t.TypeOf; + +export const ruleExecutionMetrics = t.partial({ + total_search_duration_ms: durationMetric, + total_indexing_duration_ms: durationMetric, + execution_gap_duration_s: durationMetric, +}); + +export type RuleExecutionMetrics = t.TypeOf; + +// ------------------------------------------------------------------------------------------------- +// Rule execution summary + +export const ruleExecutionSummary = t.type({ + last_execution: t.type({ + date: IsoDateString, + status: ruleExecutionStatus, + status_order: ruleExecutionStatusOrder, + message: t.string, + metrics: ruleExecutionMetrics, + }), +}); + +export type RuleExecutionSummary = t.TypeOf; + +// ------------------------------------------------------------------------------------------------- +// Rule execution events + +export const ruleExecutionEvent = t.type({ + date: IsoDateString, + status: ruleExecutionStatus, + message: t.string, +}); + +export type RuleExecutionEvent = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 7e4a4fd1295bd..f063d14822c84 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -193,19 +193,6 @@ export const status = t.keyof({ }); export type Status = t.TypeOf; -export enum RuleExecutionStatus { - 'succeeded' = 'succeeded', - 'failed' = 'failed', - 'going to run' = 'going to run', - 'partial failure' = 'partial failure', - /** - * @deprecated 'partial failure' status should be used instead - */ - 'warning' = 'warning', -} - -export const ruleExecutionStatus = enumeration('RuleExecutionStatus', RuleExecutionStatus); - export const conflicts = t.keyof({ abort: null, proceed: null }); export type Conflicts = t.TypeOf; @@ -332,30 +319,6 @@ export type UpdatedByOrNull = t.TypeOf; export const createdByOrNull = t.union([created_by, t.null]); export type CreatedByOrNull = t.TypeOf; -export const last_success_at = IsoDateString; -export type LastSuccessAt = t.TypeOf; - -export const last_success_message = t.string; -export type LastSuccessMessage = t.TypeOf; - -export const last_failure_at = IsoDateString; -export type LastFailureAt = t.TypeOf; - -export const last_failure_message = t.string; -export type LastFailureMessage = t.TypeOf; - -export const last_gap = t.string; -export type LastGap = t.TypeOf; - -export const bulk_create_time_durations = t.array(t.string); -export type BulkCreateTimeDurations = t.TypeOf; - -export const search_after_time_durations = t.array(t.string); -export type SearchAfterTimeDurations = t.TypeOf; - -export const status_date = IsoDateString; -export type StatusDate = t.TypeOf; - export const rules_installed = PositiveInteger; export const rules_updated = PositiveInteger; export const status_code = PositiveInteger; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rule_statuses_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rule_statuses_schema.ts deleted file mode 100644 index d489ad562f304..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rule_statuses_schema.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'; - -export const findRulesStatusesSchema = t.exact( - t.type({ - ids: t.array(t.string), - }) -); - -export type FindRulesStatusesSchema = t.TypeOf; - -export type FindRulesStatusesSchemaDecoded = FindRulesStatusesSchema; - -export const findRuleStatusSchema = t.exact( - t.type({ - ruleId: t.string, - }) -); - -export type FindRuleStatusSchema = t.TypeOf; - -export type FindRuleStatusSchemaDecoded = FindRuleStatusSchema; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_rule_execution_events_request.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_rule_execution_events_request.ts new file mode 100644 index 0000000000000..5e0e63ae7330d --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_rule_execution_events_request.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 * as t from 'io-ts'; + +export const GetRuleExecutionEventsRequestParams = t.exact( + t.type({ + ruleId: t.string, + }) +); + +export type GetRuleExecutionEventsRequestParams = t.TypeOf< + typeof GetRuleExecutionEventsRequestParams +>; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index 64f6c9c32d4cb..78ac7d6f1bbef 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -64,17 +64,9 @@ import { updated_by, created_at, created_by, - ruleExecutionStatus, - status_date, - last_success_at, - last_success_message, - last_failure_at, - last_failure_message, namespace, - last_gap, - bulk_create_time_durations, - search_after_time_durations, -} from '../common/schemas'; + ruleExecutionSummary, +} from '../common'; export const createSchema = < Required extends t.Props, @@ -419,16 +411,9 @@ const responseRequiredFields = { created_at, created_by, }; + const responseOptionalFields = { - status: ruleExecutionStatus, - status_date, - last_success_at, - last_success_message, - last_failure_at, - last_failure_message, - last_gap, - bulk_create_time_durations, - search_after_time_durations, + execution_summary: ruleExecutionSummary, }; export const fullResponseSchema = t.intersection([ diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/get_rule_execution_events_response.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/get_rule_execution_events_response.ts new file mode 100644 index 0000000000000..9a732a46cbaba --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/get_rule_execution_events_response.ts @@ -0,0 +1,17 @@ +/* + * 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 { ruleExecutionEvent } from '../common'; + +export const GetRuleExecutionEventsResponse = t.exact( + t.type({ + events: t.array(ruleExecutionEvent), + }) +); + +export type GetRuleExecutionEventsResponse = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts index fa8ebaf597f47..c30ea389fad76 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts @@ -6,6 +6,7 @@ */ export * from './error_schema'; +export * from './get_rule_execution_events_response'; export * from './import_rules_schema'; export * from './prepackaged_rules_schema'; export * from './prepackaged_rules_status_schema'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index 5bbad750d997d..5b25a1273ac37 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -6,7 +6,6 @@ */ import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../constants'; -import { RuleExecutionStatus } from '../common/schemas'; import { getListArrayMock } from '../types/lists.mock'; import { RulesSchema } from './rules_schema'; @@ -61,10 +60,6 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem type: 'query', threat: [], version: 1, - status: RuleExecutionStatus.succeeded, - status_date: '2020-02-22T16:47:50.047Z', - last_success_at: '2020-02-22T16:47:50.047Z', - last_success_message: 'succeeded', output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, @@ -73,6 +68,16 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem rule_id: 'query-rule-id', interval: '5m', exceptions_list: getListArrayMock(), + // TODO: https://github.com/elastic/kibana/pull/121644 clean up + // execution_summary: { + // last_execution: { + // date: '2020-02-22T16:47:50.047Z', + // status: RuleExecutionStatus.succeeded, + // status_order: 0, + // message: 'succeeded', + // metrics: {}, + // }, + // }, }); export const getRulesMlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index ac9329c3870f1..1a6c86854f6ec 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -62,12 +62,6 @@ import { timeline_id, timeline_title, threshold, - ruleExecutionStatus, - status_date, - last_success_at, - last_success_message, - last_failure_at, - last_failure_message, filters, meta, outcome, @@ -78,7 +72,8 @@ import { rule_name_override, timestamp_override, namespace, -} from '../common/schemas'; + ruleExecutionSummary, +} from '../common'; import { typeAndTimelineOnlySchema, TypeAndTimelineOnly } from './type_timeline_only_schema'; @@ -167,13 +162,7 @@ export const partialRulesSchema = t.partial({ license, throttle, rule_name_override, - status: ruleExecutionStatus, - status_date, timestamp_override, - last_success_at, - last_success_message, - last_failure_at, - last_failure_message, filters, meta, outcome, @@ -182,6 +171,7 @@ export const partialRulesSchema = t.partial({ namespace, note, uuid: id, // Move to 'required' post-migration + execution_summary: ruleExecutionSummary, }); /** diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index 8c6c3fdd961a8..de5a05edc2f8a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isEmpty, capitalize } from 'lodash'; +import { isEmpty } from 'lodash'; import type { EntriesArray, @@ -16,7 +16,7 @@ import type { import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { hasLargeValueList } from '@kbn/securitysolution-list-utils'; -import { RuleExecutionStatus, Threshold, ThresholdNormalized } from './schemas/common/schemas'; +import { Threshold, ThresholdNormalized } from './schemas/common'; export const hasLargeValueItem = ( exceptionItems: Array @@ -64,20 +64,3 @@ export const normalizeThresholdObject = (threshold: Threshold): ThresholdNormali export const normalizeMachineLearningJobIds = (value: string | string[]): string[] => Array.isArray(value) ? value : [value]; - -export const getRuleStatusText = ( - value: RuleExecutionStatus | null | undefined -): RuleExecutionStatus | null => - value === RuleExecutionStatus['partial failure'] - ? RuleExecutionStatus.warning - : value != null - ? value - : null; - -export const getCapitalizedRuleStatusText = ( - value: RuleExecutionStatus | null | undefined -): string | null => { - const status = getRuleStatusText(value); - - return status != null ? capitalize(status) : null; -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/index.tsx new file mode 100644 index 0000000000000..9178f154404fb --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/index.tsx @@ -0,0 +1,13 @@ +/* + * 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 './rule_status'; +export * from './rule_status_badge'; +export * from './rule_status_failed_callout'; + +// TODO: https://github.com/elastic/kibana/pull/121644 clean up +export * as ruleStatusI18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status.tsx similarity index 62% rename from x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status.tsx index 5273cdd6d4271..026a81c3726f5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status.tsx @@ -5,38 +5,41 @@ * 2.0. */ +import React from 'react'; import { EuiFlexItem, EuiHealth, EuiText } from '@elastic/eui'; -import React, { memo } from 'react'; + +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; import { FormattedDate } from '../../../../common/components/formatted_date'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; -import { getStatusColor } from './helpers'; +import { getStatusColor, normalizeRuleExecutionStatus } from './utils'; import * as i18n from './translations'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; interface RuleStatusProps { - children: React.ReactNode | null | undefined; - statusDate: string | null | undefined; status: RuleExecutionStatus | null | undefined; + date: string | null | undefined; + children: React.ReactNode | null | undefined; } -const RuleStatusComponent: React.FC = ({ children, statusDate, status }) => { +const RuleStatusComponent: React.FC = ({ status, date, children }) => { + const normalizedStatus = normalizeRuleExecutionStatus(status); + const statusColor = getStatusColor(normalizedStatus); return ( <> - + - {status ?? getEmptyTagValue()} + {normalizedStatus ?? getEmptyTagValue()} - {statusDate != null && status != null && ( + {date != null && normalizedStatus != null && ( <> <>{i18n.STATUS_AT} - + )} @@ -45,4 +48,5 @@ const RuleStatusComponent: React.FC = ({ children, statusDate, ); }; -export const RuleStatus = memo(RuleStatusComponent); +export const RuleStatus = React.memo(RuleStatusComponent); +RuleStatus.displayName = 'RuleStatus'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status_badge/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.test.tsx similarity index 61% rename from x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status_badge/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.test.tsx index d57bfcb329a77..52a6c723fcbd6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status_badge/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.test.tsx @@ -8,13 +8,12 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { RuleExecutionStatusBadge } from '.'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; +import { RuleStatusBadge } from './rule_status_badge'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; - -describe('Component RuleExecutionStatus', () => { - it('should render component correctly with capitalized status text', () => { - render(); +describe('RuleStatusBadge', () => { + it('renders capitalized status text', () => { + render(); expect(screen.getByText('Succeeded')).toBeInTheDocument(); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status_badge/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.tsx similarity index 53% rename from x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status_badge/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.tsx index 9203480716e48..48e744172b6ae 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status_badge/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.tsx @@ -9,12 +9,11 @@ import React from 'react'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { HealthTruncateText } from '../../../../common/components/health_truncate_text'; -import { getStatusColor } from '../rule_status/helpers'; +import { getCapitalizedRuleStatusText, getStatusColor } from './utils'; -import { getCapitalizedRuleStatusText } from '../../../../../common/detection_engine/utils'; -import type { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; +import type { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; -interface RuleExecutionStatusBadgeProps { +interface RuleStatusBadgeProps { status: RuleExecutionStatus | null | undefined; } @@ -22,19 +21,19 @@ interface RuleExecutionStatusBadgeProps { * Shows rule execution status * @param status - rule execution status */ -const RuleExecutionStatusBadgeComponent = ({ status }: RuleExecutionStatusBadgeProps) => { - const displayStatus = getCapitalizedRuleStatusText(status); +const RuleStatusBadgeComponent = ({ status }: RuleStatusBadgeProps) => { + const statusText = getCapitalizedRuleStatusText(status); + const statusColor = getStatusColor(status ?? null); return ( - {displayStatus ?? getEmptyTagValue()} + {statusText ?? getEmptyTagValue()} ); }; -export const RuleExecutionStatusBadge = React.memo(RuleExecutionStatusBadgeComponent); - -RuleExecutionStatusBadge.displayName = 'RuleExecutionStatusBadge'; +export const RuleStatusBadge = React.memo(RuleStatusBadgeComponent); +RuleStatusBadge.displayName = 'RuleStatusBadge'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx new file mode 100644 index 0000000000000..562d0f4df9872 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; + +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; +import { RuleStatusFailedCallOut } from './rule_status_failed_callout'; + +// TODO: https://github.com/elastic/kibana/pull/121644 clean up +// - switch to react testing library +// - test actual content being rendered +describe('RuleStatusFailedCallOut', () => { + describe('Visibility conditions', () => { + const renderWith = (status: RuleExecutionStatus | null | undefined) => + shallow(); + + it('is hidden if status is undefined', () => { + const wrapper = renderWith(undefined); + expect(wrapper.find('EuiCallOut')).toHaveLength(0); + }); + + it('is hidden if status is null', () => { + const wrapper = renderWith(null); + expect(wrapper.find('EuiCallOut')).toHaveLength(0); + }); + + it('is hidden if status is "going to run"', () => { + const wrapper = renderWith(RuleExecutionStatus['going to run']); + expect(wrapper.find('EuiCallOut')).toHaveLength(0); + }); + + it('is hidden if status is "succeeded"', () => { + const wrapper = renderWith(RuleExecutionStatus.succeeded); + expect(wrapper.find('EuiCallOut')).toHaveLength(0); + }); + + it('is visible if status is "warning"', () => { + const wrapper = renderWith(RuleExecutionStatus.warning); + expect(wrapper.find('EuiCallOut')).toHaveLength(1); + }); + + it('is visible if status is "partial failure"', () => { + const wrapper = renderWith(RuleExecutionStatus['partial failure']); + expect(wrapper.find('EuiCallOut')).toHaveLength(1); + }); + + it('is visible if status is "failed"', () => { + const wrapper = renderWith(RuleExecutionStatus.failed); + expect(wrapper.find('EuiCallOut')).toHaveLength(1); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx new file mode 100644 index 0000000000000..474ce6ac0ffe0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx @@ -0,0 +1,81 @@ +/* + * 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 React from 'react'; + +import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedDate } from '../../../../common/components/formatted_date'; + +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; +import { normalizeRuleExecutionStatus } from './utils'; + +import * as i18n from './translations'; + +interface RuleStatusFailedCallOutProps { + date: string; + message: string; + status?: RuleExecutionStatus | null; +} + +const RuleStatusFailedCallOutComponent: React.FC = ({ + date, + message, + status, +}) => { + // TODO: https://github.com/elastic/kibana/pull/121644 clean up + const props = getPropsByStatus(status); + if (!props) { + // we will not show this callout for this status + return null; + } + + return ( + + {props.title} + + + + + } + color={props.color} + iconType="alert" + > +

{message}

+
+ ); +}; + +export const RuleStatusFailedCallOut = React.memo(RuleStatusFailedCallOutComponent); +RuleStatusFailedCallOut.displayName = 'RuleStatusFailedCallOut'; + +// ------------------------------------------------------------------------------------------------- +// Helpers + +interface HelperProps { + color: 'danger' | 'warning'; + title: string; +} + +const getPropsByStatus = (status: RuleExecutionStatus | null | undefined): HelperProps | null => { + const normalizedStatus = normalizeRuleExecutionStatus(status); + switch (normalizedStatus) { + case RuleExecutionStatus.failed: + return { + color: 'danger', + title: i18n.ERROR_CALLOUT_TITLE, + }; + case RuleExecutionStatus.warning: + return { + color: 'warning', + title: i18n.PARTIAL_FAILURE_CALLOUT_TITLE, + }; + default: + return null; + } +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/translations.ts similarity index 71% rename from x-pack/plugins/security_solution/public/detections/components/rules/rule_status/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/translations.ts index b4c5475f9ed84..e2a1e566813a3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/translations.ts @@ -34,3 +34,17 @@ export const REFRESH = i18n.translate( defaultMessage: 'Refresh', } ); + +export const ERROR_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleStatus.errorCalloutTitle', + { + defaultMessage: 'Rule failure at', + } +); + +export const PARTIAL_FAILURE_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleStatus.partialErrorCalloutTitle', + { + defaultMessage: 'Warning at', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.test.tsx new file mode 100644 index 0000000000000..d2bbe11bafabe --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.test.tsx @@ -0,0 +1,16 @@ +/* + * 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 { getStatusColor } from './utils'; + +describe('Rule execution status utils', () => { + describe('getStatusColor', () => { + it('returns "subdued" if null was provided', () => { + expect(getStatusColor(null)).toBe('subdued'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.ts similarity index 55% rename from x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.ts index 13381b350761b..5f68f6634fff7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.ts @@ -5,7 +5,24 @@ * 2.0. */ -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { capitalize } from 'lodash'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; + +export const normalizeRuleExecutionStatus = ( + value: RuleExecutionStatus | null | undefined +): RuleExecutionStatus | null => + value === RuleExecutionStatus['partial failure'] + ? RuleExecutionStatus.warning + : value != null + ? value + : null; + +export const getCapitalizedRuleStatusText = ( + value: RuleExecutionStatus | null | undefined +): string | null => { + const status = normalizeRuleExecutionStatus(value); + return status != null ? capitalize(status) : null; +}; export const getStatusColor = (status: RuleExecutionStatus | string | null) => status == null diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts index 90e972ef90f5b..6f01001b3f957 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts @@ -5,22 +5,26 @@ * 2.0. */ +import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common'; +import { + GetRuleExecutionEventsResponse, + RulesSchema, +} from '../../../../../../common/detection_engine/schemas/response'; + +import { getRulesSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; +import { savedRuleMock, rulesMock } from '../mock'; + import { PatchRuleProps, CreateRulesProps, UpdateRulesProps, PrePackagedRulesStatusResponse, BasicFetchProps, - RuleStatusResponse, Rule, FetchRuleProps, FetchRulesResponse, FetchRulesProps, } from '../types'; -import { savedRuleMock, rulesMock } from '../mock'; -import { getRulesSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; -import { RulesSchema } from '../../../../../../common/detection_engine/schemas/response'; -import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common/schemas'; export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise => Promise.resolve(getRulesSchemaMock()); @@ -49,58 +53,6 @@ export const getPrePackagedRulesStatus = async ({ export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise => Promise.resolve(true); -export const getRuleStatusById = async ({ - id, - signal, -}: { - id: string; - signal: AbortSignal; -}): Promise => - Promise.resolve({ - myOwnRuleID: { - current_status: { - alert_id: 'alertId', - status_date: 'mm/dd/yyyyTHH:MM:sssz', - status: RuleExecutionStatus.succeeded, - last_failure_at: null, - last_success_at: 'mm/dd/yyyyTHH:MM:sssz', - last_failure_message: null, - last_success_message: 'it is a success', - gap: null, - bulk_create_time_durations: ['2235.01'], - search_after_time_durations: ['616.97'], - last_look_back_date: '2020-03-19T00:32:07.996Z', // NOTE: This is no longer used on the UI, but left here in case users are using it within the API - }, - failures: [], - }, - }); - -export const getRulesStatusByIds = async ({ - ids, - signal, -}: { - ids: string[]; - signal: AbortSignal; -}): Promise => - Promise.resolve({ - '12345678987654321': { - current_status: { - alert_id: 'alertId', - status_date: 'mm/dd/yyyyTHH:MM:sssz', - status: RuleExecutionStatus.succeeded, - last_failure_at: null, - last_success_at: 'mm/dd/yyyyTHH:MM:sssz', - last_failure_message: null, - last_success_message: 'it is a success', - gap: null, - bulk_create_time_durations: ['2235.01'], - search_after_time_durations: ['616.97'], - last_look_back_date: '2020-03-19T00:32:07.996Z', // NOTE: This is no longer used on the UI, but left here in case users are using it within the API - }, - failures: [], - }, - }); - export const fetchRuleById = jest.fn( async ({ id, signal }: FetchRuleProps): Promise => savedRuleMock ); @@ -122,5 +74,23 @@ export const fetchRules = async ({ signal, }: FetchRulesProps): Promise => Promise.resolve(rulesMock); +export const fetchRuleExecutionEvents = async ({ + ruleId, + signal, +}: { + ruleId: string; + signal?: AbortSignal; +}): Promise => { + return Promise.resolve({ + events: [ + { + date: '2021-12-29T10:42:59.996Z', + status: RuleExecutionStatus.succeeded, + message: 'Rule executed successfully', + }, + ], + }); +}; + export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise => Promise.resolve(['elastic', 'love', 'quality', 'code']); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index ef9120aa829e3..3591d49b66b40 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -18,7 +18,7 @@ import { createPrepackagedRules, importRules, exportRules, - getRuleStatusById, + fetchRuleExecutionEvents, fetchTags, getPrePackagedRulesStatus, previewRule, @@ -655,39 +655,32 @@ describe('Detections Rules API', () => { }); }); - describe('getRuleStatusById', () => { - const statusMock = { - myRule: { - current_status: { - alert_id: 'alertId', - status_date: 'mm/dd/yyyyTHH:MM:sssz', - status: 'succeeded', - last_failure_at: null, - last_success_at: 'mm/dd/yyyyTHH:MM:sssz', - last_failure_message: null, - last_success_message: 'it is a success', - }, - failures: [], - }, + // TODO: https://github.com/elastic/kibana/pull/121644 clean up + describe('fetchRuleExecutionEvents', () => { + const responseMock = { + dummy: 'response', }; beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(statusMock); + fetchMock.mockResolvedValue(responseMock); }); - test('check parameter url, query', async () => { - await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/internal/detection_engine/rules/_find_status', { - body: '{"ruleId":"mySuperRuleId"}', - method: 'POST', - signal: abortCtrl.signal, - }); + test('calls API with correct parameters', async () => { + await fetchRuleExecutionEvents({ ruleId: '42', signal: abortCtrl.signal }); + + expect(fetchMock).toHaveBeenCalledWith( + '/internal/detection_engine/rules/42/execution/events', + { + method: 'GET', + signal: abortCtrl.signal, + } + ); }); - test('happy path', async () => { - const ruleResp = await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); - expect(ruleResp).toEqual(statusMock); + test('returns API response as is', async () => { + const response = await fetchRuleExecutionEvents({ ruleId: '42', signal: abortCtrl.signal }); + expect(response).toEqual(responseMock); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 6444969e123ad..6ff9b2a288452 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -6,11 +6,8 @@ */ import { camelCase } from 'lodash'; -import { - FullResponseSchema, - PreviewResponse, -} from '../../../../../common/detection_engine/schemas/request'; -import { HttpStart } from '../../../../../../../../src/core/public'; +import { HttpStart } from 'src/core/public'; + import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_PREPACKAGED_URL, @@ -18,8 +15,18 @@ import { DETECTION_ENGINE_TAGS_URL, DETECTION_ENGINE_RULES_BULK_ACTION, DETECTION_ENGINE_RULES_PREVIEW, - INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL, + detectionEngineRuleExecutionEventsUrl, } from '../../../../../common/constants'; +import { BulkAction } from '../../../../../common/detection_engine/schemas/common'; +import { + FullResponseSchema, + PreviewResponse, +} from '../../../../../common/detection_engine/schemas/request'; +import { + RulesSchema, + GetRuleExecutionEventsResponse, +} from '../../../../../common/detection_engine/schemas/response'; + import { UpdateRulesProps, CreateRulesProps, @@ -33,7 +40,6 @@ import { BasicFetchProps, ImportDataProps, ExportDocumentsProps, - RuleStatusResponse, ImportDataResponse, PrePackagedRulesStatusResponse, BulkRuleResponse, @@ -44,9 +50,7 @@ import { } from './types'; import { KibanaServices } from '../../../../common/lib/kibana'; import * as i18n from '../../../pages/detection_engine/rules/translations'; -import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; import { convertRulesFilterToKQL } from './utils'; -import { BulkAction } from '../../../../../common/detection_engine/schemas/common/schemas'; /** * Create provided Rule @@ -241,15 +245,7 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise => - KibanaServices.get().http.fetch(INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL, { - method: 'POST', - body: JSON.stringify({ ruleId: id }), + ruleId: string; + signal?: AbortSignal; +}): Promise => { + const url = detectionEngineRuleExecutionEventsUrl(ruleId); + return KibanaServices.get().http.fetch(url, { + method: 'GET', signal, }); +}; /** * Fetch all unique Tags used by Rules diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts index 8128eb045f759..435a1e6ef073b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts @@ -12,4 +12,4 @@ export * from './types'; export * from './use_rule'; export * from './rules_table'; export * from './use_pre_packaged_rules'; -export * from './use_rule_status'; +export * from './use_rule_execution_events'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 2507d5a9596b6..9aecab3d06a02 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -30,9 +30,8 @@ import { timestamp_override, threshold, BulkAction, - ruleExecutionStatus, - RuleExecutionStatus, -} from '../../../../../common/detection_engine/schemas/common/schemas'; + ruleExecutionSummary, +} from '../../../../../common/detection_engine/schemas/common'; import { CreateRulesSchema, PatchRulesSchema, @@ -122,21 +121,12 @@ export const RuleSchema = t.intersection([ index: t.array(t.string), language: t.string, license, - last_failure_at: t.string, - last_failure_message: t.string, - last_success_message: t.string, - last_success_at: t.string, - last_gap: t.string, - bulk_create_time_durations: t.array(t.string), - search_after_time_durations: t.array(t.string), meta: MetaRule, machine_learning_job_id: t.array(t.string), output_index: t.string, query: t.string, rule_name_override, saved_id: t.string, - status: ruleExecutionStatus, - status_date: t.string, threshold, threat_query, threat_filters, @@ -151,6 +141,8 @@ export const RuleSchema = t.intersection([ exceptions_list: listArray, uuid: t.string, version: t.number, + // TODO: https://github.com/elastic/kibana/pull/121644 clean up + execution_summary: ruleExecutionSummary, }), ]); @@ -291,26 +283,6 @@ export interface ExportDocumentsProps { signal?: AbortSignal; } -export interface RuleStatus { - current_status: RuleInfoStatus; - failures: RuleInfoStatus[]; -} -export interface RuleInfoStatus { - alert_id: string; - status_date: string; - status: RuleExecutionStatus | null; - last_failure_at: string | null; - last_success_at: string | null; - last_failure_message: string | null; - last_success_message: string | null; - last_look_back_date: string | null | undefined; // NOTE: This is no longer used on the UI, but left here in case users are using it within the API - gap: string | null | undefined; - bulk_create_time_durations: string[] | null | undefined; - search_after_time_durations: string[] | null | undefined; -} - -export type RuleStatusResponse = Record; - export interface PrePackagedRulesStatusResponse { rules_custom_installed: number; rules_installed: number; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx new file mode 100644 index 0000000000000..2036c79d3f80c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 React from 'react'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { renderHook, cleanup } from '@testing-library/react-hooks'; + +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useRuleExecutionEvents } from './use_rule_execution_events'; + +import * as api from './api'; + +jest.mock('./api'); +jest.mock('../../../../common/hooks/use_app_toasts'); + +// TODO: https://github.com/elastic/kibana/pull/121644 clean up +describe('useRuleExecutionEvents', () => { + (useAppToasts as jest.Mock).mockReturnValue(useAppToastsMock.create()); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(async () => { + cleanup(); + }); + + it('fetches data from the API (via fetchRuleExecutionEvents)', async () => { + const fetchRuleExecutionEvents = jest.spyOn(api, 'fetchRuleExecutionEvents'); + const queryClient = new QueryClient(); + const wrapper: React.FC = ({ children }) => ( + {children} + ); + + const { result, waitFor } = renderHook(() => useRuleExecutionEvents('some rule ID'), { + wrapper, + }); + + await waitFor(() => result.current.isSuccess); + + expect(fetchRuleExecutionEvents).toHaveBeenCalledTimes(1); + expect(result.current.isLoading).toEqual(false); + expect(result.current.data).toEqual([ + { + date: '2021-12-29T10:42:59.996Z', + status: RuleExecutionStatus.succeeded, + message: 'Rule executed successfully', + }, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx new file mode 100644 index 0000000000000..8cbb9cb3e9ceb --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx @@ -0,0 +1,31 @@ +/* + * 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 { useQuery } from 'react-query'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { fetchRuleExecutionEvents } from './api'; +import * as i18n from './translations'; + +// TODO: https://github.com/elastic/kibana/pull/121644 clean up +export const useRuleExecutionEvents = (ruleId: string) => { + const { addError } = useAppToasts(); + + return useQuery( + 'ruleExecutionEvents', + async ({ signal }) => { + const response = await fetchRuleExecutionEvents({ ruleId, signal }); + return response.events; + }, + { + onError: (e) => { + // TODO: Should it be responsible for showing toasts? + // TODO: Change the title + addError(e, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }); + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx deleted file mode 100644 index 73b05f94153e6..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx +++ /dev/null @@ -1,85 +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 { renderHook, act, cleanup } from '@testing-library/react-hooks'; -import { useRuleStatus, ReturnRuleStatus } from './use_rule_status'; -import * as api from './api'; -import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; - -jest.mock('./api'); -jest.mock('../../../../common/hooks/use_app_toasts'); - -describe('useRuleStatus', () => { - (useAppToasts as jest.Mock).mockReturnValue(useAppToastsMock.create()); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - afterEach(async () => { - cleanup(); - }); - - describe('useRuleStatus', () => { - test('init', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useRuleStatus('myOwnRuleID') - ); - await waitForNextUpdate(); - expect(result.current).toEqual([true, null, null]); - }); - }); - - test('fetch rule status', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useRuleStatus('myOwnRuleID') - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual([ - false, - { - current_status: { - alert_id: 'alertId', - last_failure_at: null, - last_failure_message: null, - last_success_at: 'mm/dd/yyyyTHH:MM:sssz', - last_success_message: 'it is a success', - status: 'succeeded', - status_date: 'mm/dd/yyyyTHH:MM:sssz', - gap: null, - bulk_create_time_durations: ['2235.01'], - search_after_time_durations: ['616.97'], - last_look_back_date: '2020-03-19T00:32:07.996Z', // NOTE: This is no longer used on the UI, but left here in case users are using it within the API - }, - failures: [], - }, - result.current[2], - ]); - }); - }); - - test('re-fetch rule status', async () => { - const spyOngetRuleStatusById = jest.spyOn(api, 'getRuleStatusById'); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useRuleStatus('myOwnRuleID') - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - if (result.current[2]) { - result.current[2]('myOwnRuleID'); - } - await waitForNextUpdate(); - expect(spyOngetRuleStatusById).toHaveBeenCalledTimes(2); - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx deleted file mode 100644 index 815ed261c490f..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx +++ /dev/null @@ -1,67 +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 { useEffect, useRef, useState } from 'react'; -import { isNotFoundError } from '@kbn/securitysolution-t-grid'; -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; - -import { getRuleStatusById } from './api'; -import * as i18n from './translations'; -import { RuleStatus } from './types'; - -type Func = (ruleId: string) => void; -export type ReturnRuleStatus = [boolean, RuleStatus | null, Func | null]; - -/** - * Hook for using to get a Rule from the Detection Engine API - * - * @param id desired Rule ID's (not rule_id) - * - */ -export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus => { - const [ruleStatus, setRuleStatus] = useState(null); - const fetchRuleStatus = useRef(null); - const [loading, setLoading] = useState(true); - const { addError } = useAppToasts(); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - const fetchData = async (idToFetch: string) => { - try { - setLoading(true); - const ruleStatusResponse = await getRuleStatusById({ - id: idToFetch, - signal: abortCtrl.signal, - }); - - if (isSubscribed) { - setRuleStatus(ruleStatusResponse[id ?? '']); - } - } catch (error) { - if (isSubscribed && !isNotFoundError(error)) { - setRuleStatus(null); - addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }); - } - } - if (isSubscribed) { - setLoading(false); - } - }; - if (id != null) { - fetchData(id); - } - fetchRuleStatus.current = fetchData; - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [id, addError]); - - return [loading, ruleStatus, fetchRuleStatus.current]; -}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx index f1771e6f44ddd..d03e61334b728 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import React, { Dispatch } from 'react'; import { EuiBasicTableColumn, EuiTableActionsColumnType, @@ -14,9 +15,11 @@ import { EuiBadge, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { sum } from 'lodash'; -import React, { Dispatch } from 'react'; +import { + RuleExecutionSummary, + DurationMetric, +} from '../../../../../../common/detection_engine/schemas/common'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { Rule } from '../../../../containers/detection_engine/rules'; import { getEmptyTagValue } from '../../../../../common/components/empty_value'; @@ -26,7 +29,7 @@ import { ActionToaster } from '../../../../../common/components/toasters'; import { PopoverItems } from '../../../../../common/components/popover_items'; import { RuleSwitch } from '../../../../components/rules/rule_switch'; import { SeverityBadge } from '../../../../components/rules/severity_badge'; -import { RuleExecutionStatusBadge } from '../../../../components/rules/rule_execution_status_badge'; +import { RuleStatusBadge } from '../../../../components/rules/rule_execution_status'; import * as i18n from '../translations'; import { deleteRulesAction, @@ -271,9 +274,9 @@ export const getRulesColumns = (columnsProps: GetColumnsProps): TableColumn[] => truncateText: true, }, { - field: 'status_date', + field: 'execution_summary.last_execution.date', name: i18n.COLUMN_LAST_COMPLETE_RUN, - render: (value: Rule['status_date']) => { + render: (value: RuleExecutionSummary['last_execution']['date'] | undefined) => { return value == null ? ( getEmptyTagValue() ) : ( @@ -289,9 +292,11 @@ export const getRulesColumns = (columnsProps: GetColumnsProps): TableColumn[] => truncateText: true, }, { - field: 'status', + field: 'execution_summary.last_execution.status', name: i18n.COLUMN_LAST_RESPONSE, - render: (value: Rule['status']) => , + render: (value: RuleExecutionSummary['last_execution']['status'] | undefined) => ( + + ), width: '16%', truncateText: true, }, @@ -340,39 +345,39 @@ export const getMonitoringColumns = (columnsProps: GetColumnsProps): TableColumn { ...getColumnRuleName(columnsProps), width: '28%' }, getColumnTags(), { - field: 'bulk_create_time_durations', + field: 'execution_summary.last_execution.metrics.total_indexing_duration_ms', name: ( ), - render: (value: Rule['bulk_create_time_durations'] | undefined) => ( - - {value?.length ? sum(value.map(Number)).toFixed() : getEmptyTagValue()} + render: (value: DurationMetric | undefined) => ( + + {value != null ? value.toFixed() : getEmptyTagValue()} ), width: '16%', truncateText: true, }, { - field: 'search_after_time_durations', + field: 'execution_summary.last_execution.metrics.total_search_duration_ms', name: ( ), - render: (value: Rule['search_after_time_durations'] | undefined) => ( - - {value?.length ? sum(value.map(Number)).toFixed() : getEmptyTagValue()} + render: (value: DurationMetric | undefined) => ( + + {value != null ? value.toFixed() : getEmptyTagValue()} ), width: '14%', truncateText: true, }, { - field: 'last_gap', + field: 'execution_summary.last_execution.metrics.execution_gap_duration_s', name: ( ), - render: (value: Rule['last_gap'] | undefined) => ( + render: (value: DurationMetric | undefined) => ( - {value ?? getEmptyTagValue()} + {value != null ? value.toFixed() : getEmptyTagValue()} ), width: '14%', truncateText: true, }, { - field: 'status', + field: 'execution_summary.last_execution.status', name: i18n.COLUMN_LAST_RESPONSE, - render: (value: Rule['status'] | undefined) => , + render: (value: RuleExecutionSummary['last_execution']['status'] | undefined) => ( + + ), width: '12%', truncateText: true, }, { - field: 'status_date', + field: 'execution_summary.last_execution.date', name: i18n.COLUMN_LAST_COMPLETE_RUN, - render: (value: Rule['status_date'] | undefined) => { + render: (value: RuleExecutionSummary['last_execution']['date'] | undefined) => { return value == null ? ( getEmptyTagValue() ) : ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx deleted file mode 100644 index c91aade50cbae..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx +++ /dev/null @@ -1,88 +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 React from 'react'; -import { shallow, mount } from 'enzyme'; -import { - TestProviders, - createSecuritySolutionStorageMock, - kibanaObservable, - mockGlobalState, - SUB_PLUGINS_REDUCER, -} from '../../../../../common/mock'; -import { FailureHistory } from './failure_history'; -import { useRuleStatus } from '../../../../containers/detection_engine/rules'; -jest.mock('../../../../containers/detection_engine/rules'); - -import { waitFor } from '@testing-library/react'; - -import '../../../../../common/mock/match_media'; - -import { createStore, State } from '../../../../../common/store'; -import { mockHistory, Router } from '../../../../../common/mock/router'; - -const state: State = { - ...mockGlobalState, -}; -const { storage } = createSecuritySolutionStorageMock(); -const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - -describe('RuleDetailsPageComponent', () => { - beforeAll(() => { - (useRuleStatus as jest.Mock).mockReturnValue([ - false, - { - status: 'succeeded', - last_failure_at: new Date().toISOString(), - last_failure_message: 'my fake failure message', - failures: [ - { - alert_id: 'myfakeid', - status_date: new Date().toISOString(), - status: 'failed', - last_failure_at: new Date().toISOString(), - last_success_at: new Date().toISOString(), - last_failure_message: 'my fake failure message', - last_look_back_date: new Date().toISOString(), // NOTE: This is no longer used on the UI, but left here in case users are using it within the API - }, - ], - }, - ]); - }); - - it('renders reported rule failures correctly', async () => { - const wrapper = mount( - - - - - - ); - - await waitFor(() => { - expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - // ensure the expected error message is displayed in the table - expect(wrapper.find('EuiTableRowCell').at(2).find('div').at(1).text()).toEqual( - 'my fake failure message' - ); - }); - }); -}); - -describe('FailureHistory', () => { - beforeAll(() => { - (useRuleStatus as jest.Mock).mockReturnValue([false, null]); - }); - - it('renders correctly with no statuses', () => { - const wrapper = shallow(, { - wrappingComponent: TestProviders, - }); - - expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx index 5289e34b10046..62992d5d189c1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import React from 'react'; import { EuiBasicTable, EuiPanel, @@ -12,25 +13,48 @@ import { EuiHealth, EuiBasicTableColumn, } from '@elastic/eui'; -import React, { memo } from 'react'; -import { useRuleStatus, RuleInfoStatus } from '../../../../containers/detection_engine/rules'; +import { RuleExecutionEvent } from '../../../../../../common/detection_engine/schemas/common'; +import { useRuleExecutionEvents } from '../../../../containers/detection_engine/rules'; import { HeaderSection } from '../../../../../common/components/header_section'; import * as i18n from './translations'; import { FormattedDate } from '../../../../../common/components/formatted_date'; +// TODO: https://github.com/elastic/kibana/pull/121644 clean up +const columns: Array> = [ + { + name: i18n.COLUMN_STATUS_TYPE, + render: () => {i18n.TYPE_FAILED}, + truncateText: false, + width: '16%', + }, + { + field: 'date', + name: i18n.COLUMN_FAILED_AT, + render: (value: string) => , + sortable: false, + truncateText: false, + width: '24%', + }, + { + field: 'message', + name: i18n.COLUMN_FAILED_MSG, + render: (value: string) => <>{value}, + sortable: false, + truncateText: false, + width: '60%', + }, +]; + interface FailureHistoryProps { - id?: string | null; + ruleId: string; } -const renderStatus = () => {i18n.TYPE_FAILED}; -const renderLastFailureAt = (value: string) => ( - -); -const renderLastFailureMessage = (value: string) => <>{value}; +const FailureHistoryComponent: React.FC = ({ ruleId }) => { + const events = useRuleExecutionEvents(ruleId); + const loading = events.isLoading; + const items = events.data ?? []; -const FailureHistoryComponent: React.FC = ({ id }) => { - const [loading, ruleStatus] = useRuleStatus(id); if (loading) { return ( @@ -39,43 +63,19 @@ const FailureHistoryComponent: React.FC = ({ id }) => { ); } - const columns: Array> = [ - { - name: i18n.COLUMN_STATUS_TYPE, - render: renderStatus, - truncateText: false, - width: '16%', - }, - { - field: 'last_failure_at', - name: i18n.COLUMN_FAILED_AT, - render: renderLastFailureAt, - sortable: false, - truncateText: false, - width: '24%', - }, - { - field: 'last_failure_message', - name: i18n.COLUMN_FAILED_MSG, - render: renderLastFailureMessage, - sortable: false, - truncateText: false, - width: '60%', - }, - ]; + return ( rs.last_failure_at != null) : [] - } - sorting={{ sort: { field: 'status_date', direction: 'desc' } }} + sorting={{ sort: { field: 'date', direction: 'desc' } }} /> ); }; -export const FailureHistory = memo(FailureHistoryComponent); +export const FailureHistory = React.memo(FailureHistoryComponent); +FailureHistory.displayName = 'FailureHistory'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index 11529d2f4c6c5..21666ae3ad3d1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -20,7 +20,6 @@ import { import { RuleDetailsPage } from './index'; import { createStore, State } from '../../../../../common/store'; import { useUserData } from '../../../../components/user_info'; -import { useRuleStatus } from '../../../../containers/detection_engine/rules'; import { useRuleWithFallback } from '../../../../containers/detection_engine/rules/use_rule_with_fallback'; import { useSourcererDataView } from '../../../../../common/containers/sourcerer'; @@ -131,15 +130,6 @@ describe('RuleDetailsPageComponent', () => { indicesExist: true, indexPattern: {}, }); - (useRuleStatus as jest.Mock).mockReturnValue([ - false, - { - status: 'succeeded', - last_failure_at: new Date().toISOString(), - last_failure_message: 'my fake failure message', - failures: [], - }, - ]); (useRuleWithFallback as jest.Mock).mockReturnValue({ error: null, loading: false, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 03d43e3969c10..7039a2eeb496a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -27,7 +27,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useParams } from 'react-router-dom'; import { connect, ConnectedProps, useDispatch } from 'react-redux'; import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; import { ExceptionListTypeEnum, ExceptionListIdentifiers, @@ -52,7 +51,7 @@ import { } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../../../common/components/search_bar'; import { SecuritySolutionPageWrapper } from '../../../../../common/components/page_wrapper'; -import { Rule, useRuleStatus, RuleInfoStatus } from '../../../../containers/detection_engine/rules'; +import { Rule } from '../../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; import { StepAboutRuleToggleDetails } from '../../../../components/rules/step_about_rule_details'; @@ -75,9 +74,6 @@ import { useGlobalTime } from '../../../../../common/containers/use_global_time' import { inputsSelectors } from '../../../../../common/store/inputs'; import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; import { RuleActionsOverflow } from '../../../../components/rules/rule_actions_overflow'; -import { RuleStatusFailedCallOut } from './status_failed_callout'; -import { FailureHistory } from './failure_history'; -import { RuleStatus } from '../../../../components/rules//rule_status'; import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions'; import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license'; @@ -109,12 +105,17 @@ import { isBoolean, } from '../../../../../common/utils/privileges'; +import { + RuleStatus, + RuleStatusFailedCallOut, + ruleStatusI18n, +} from '../../../../components/rules/rule_execution_status'; +import { FailureHistory } from './failure_history'; + import * as detectionI18n from '../../translations'; import * as ruleI18n from '../translations'; -import * as statusI18n from '../../../../components/rules/rule_status/translations'; import * as i18n from './translations'; import { NeedAdminForUpdateRulesCallOut } from '../../../../components/callouts/need_admin_for_update_callout'; -import { getRuleStatusText } from '../../../../../../common/detection_engine/utils'; import { MissingPrivilegesCallOut } from '../../../../components/callouts/missing_privileges_callout'; import { useRuleWithFallback } from '../../../../containers/detection_engine/rules/use_rule_with_fallback'; import { BadgeOptions } from '../../../../../common/components/header_page/types'; @@ -224,15 +225,6 @@ const RuleDetailsPageComponent: React.FC = ({ isExistingRule, } = useRuleWithFallback(ruleId); const { pollForSignalIndex } = useSignalHelpers(); - const [loadingStatus, ruleStatus, fetchRuleStatus] = useRuleStatus(ruleId); - const [currentStatus, setCurrentStatus] = useState( - ruleStatus?.current_status ?? null - ); - useEffect(() => { - if (!deepEqual(currentStatus, ruleStatus?.current_status)) { - setCurrentStatus(ruleStatus?.current_status ?? null); - } - }, [currentStatus, ruleStatus, setCurrentStatus]); const [rule, setRule] = useState(null); const isLoading = ruleLoading && rule == null; // This is used to re-trigger api rule status when user de/activate rule @@ -466,27 +458,25 @@ const RuleDetailsPageComponent: React.FC = ({ : DEFAULT_INDEX_PATTERN), [rule?.index, uebaEnabled] ); - const handleRefresh = useCallback(() => { - if (fetchRuleStatus != null && ruleId != null) { - fetchRuleStatus(ruleId); - } - }, [fetchRuleStatus, ruleId]); + const lastExecution = rule?.execution_summary?.last_execution; + const lastExecutionStatus = lastExecution?.status; + const lastExecutionDate = lastExecution?.date ?? ''; + const lastExecutionMessage = lastExecution?.message ?? ''; + + // TODO: https://github.com/elastic/kibana/pull/121644 clean up const ruleStatusInfo = useMemo(() => { - return loadingStatus ? ( + return ruleLoading ? ( ) : ( <> - + = ({ ); - }, [isExistingRule, currentStatus, loadingStatus, handleRefresh]); + }, [lastExecutionStatus, lastExecutionDate, ruleLoading, isExistingRule, refreshRule]); + // TODO: https://github.com/elastic/kibana/pull/121644 clean up const ruleError = useMemo(() => { - if (loadingStatus) { + if (ruleLoading) { return ( ); - } else if ( - currentStatus?.status === 'failed' && - (ruleDetailTab === RuleDetailTabs.alerts || ruleDetailTab === RuleDetailTabs.failures) && - currentStatus?.last_failure_at != null - ) { - return ( - - ); - } else if ( - (currentStatus?.status === 'warning' || currentStatus?.status === 'partial failure') && - (ruleDetailTab === RuleDetailTabs.alerts || ruleDetailTab === RuleDetailTabs.failures) && - currentStatus?.last_success_at != null - ) { - return ( - - ); } - return null; - }, [ruleDetailTab, currentStatus, loadingStatus]); + + return ( + + ); + }, [lastExecutionStatus, lastExecutionDate, lastExecutionMessage, ruleLoading]); const updateDateRangeCallback = useCallback( ({ x }) => { @@ -704,7 +679,7 @@ const RuleDetailsPageComponent: React.FC = ({ <> - {statusI18n.STATUS} + {ruleStatusI18n.STATUS} {':'} {ruleStatusInfo} @@ -859,7 +834,7 @@ const RuleDetailsPageComponent: React.FC = ({ onRuleChange={refreshRule} /> )} - {ruleDetailTab === RuleDetailTabs.failures && } + {ruleDetailTab === RuleDetailTabs.failures && } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx deleted file mode 100644 index 2a5c5cff30f2d..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx +++ /dev/null @@ -1,26 +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 React from 'react'; -import { shallow } from 'enzyme'; - -import { RuleStatusFailedCallOut } from './status_failed_callout'; - -describe('RuleStatusFailedCallOut', () => { - it('renders correctly', () => { - const wrapper = shallow(); - - expect(wrapper.find('EuiCallOut')).toHaveLength(1); - }); - it('renders correctly with optional params', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find('EuiCallOut')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx deleted file mode 100644 index 103cbb7a1674c..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx +++ /dev/null @@ -1,43 +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 { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { memo } from 'react'; - -import { FormattedDate } from '../../../../../common/components/formatted_date'; -import * as i18n from './translations'; - -interface RuleStatusFailedCallOutComponentProps { - date: string; - message: string; - color?: 'danger' | 'primary' | 'success' | 'warning'; -} - -const RuleStatusFailedCallOutComponent: React.FC = ({ - date, - message, - color, -}) => ( - - - {color === 'warning' ? i18n.PARTIAL_FAILURE_CALLOUT_TITLE : i18n.ERROR_CALLOUT_TITLE} - - - - - - } - color={color ? color : 'danger'} - iconType="alert" - > -

{message}

- -); - -export const RuleStatusFailedCallOut = memo(RuleStatusFailedCallOutComponent); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts index a83647f8a9781..2ea37ccfd343b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts @@ -42,20 +42,6 @@ export const UNKNOWN = i18n.translate( } ); -export const ERROR_CALLOUT_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetails.errorCalloutTitle', - { - defaultMessage: 'Rule failure at', - } -); - -export const PARTIAL_FAILURE_CALLOUT_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetails.partialErrorCalloutTitle', - { - defaultMessage: 'Warning at', - } -); - export const FAILURE_HISTORY_TAB = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.failureHistoryTab', { diff --git a/x-pack/plugins/security_solution/server/config.mock.ts b/x-pack/plugins/security_solution/server/config.mock.ts index 9325f2f57bdd6..bc83a721cbf58 100644 --- a/x-pack/plugins/security_solution/server/config.mock.ts +++ b/x-pack/plugins/security_solution/server/config.mock.ts @@ -11,7 +11,6 @@ import { parseExperimentalConfigValue, } from '../common/experimental_features'; import { ConfigType } from './config'; -import { UnderlyingLogClient } from './lib/detection_engine/rule_execution_log/types'; export const createMockConfig = (): ConfigType => { const enableExperimental: string[] = []; @@ -28,9 +27,6 @@ export const createMockConfig = (): ConfigType => { alertIgnoreFields: [], prebuiltRulesFromFileSystem: true, prebuiltRulesFromSavedObjects: false, - ruleExecutionLog: { - underlyingClient: UnderlyingLogClient.savedObjects, - }, experimentalFeatures: parseExperimentalConfigValue(enableExperimental), }; diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts index b76edf3b50800..d7e8547fd50ef 100644 --- a/x-pack/plugins/security_solution/server/config.ts +++ b/x-pack/plugins/security_solution/server/config.ts @@ -14,7 +14,6 @@ import { isValidExperimentalValue, parseExperimentalConfigValue, } from '../common/experimental_features'; -import { UnderlyingLogClient } from './lib/detection_engine/rule_execution_log/types'; const allowedExperimentalValues = getExperimentalAllowedValues(); @@ -105,19 +104,6 @@ export const configSchema = schema.object({ }, }), - /** - * Rule Execution Log Configuration - */ - ruleExecutionLog: schema.object({ - underlyingClient: schema.oneOf( - [ - schema.literal(UnderlyingLogClient.eventLog), - schema.literal(UnderlyingLogClient.savedObjects), - ], - { defaultValue: UnderlyingLogClient.eventLog } - ), - }), - /** * Artifacts Configuration */ diff --git a/x-pack/plugins/security_solution/server/index.ts b/x-pack/plugins/security_solution/server/index.ts index 777bc27a06866..f3ce6e458fe7d 100644 --- a/x-pack/plugins/security_solution/server/index.ts +++ b/x-pack/plugins/security_solution/server/index.ts @@ -20,7 +20,7 @@ export const config: PluginConfigDescriptor = { enableExperimental: true, }, schema: configSchema, - deprecations: ({ renameFromRoot }) => [ + deprecations: ({ renameFromRoot, unused }) => [ renameFromRoot('xpack.siem.enabled', 'xpack.securitySolution.enabled', { level: 'critical' }), renameFromRoot( 'xpack.siem.maxRuleImportExportSize', @@ -47,6 +47,7 @@ export const config: PluginConfigDescriptor = { `xpack.securitySolution.${SIGNALS_INDEX_KEY}`, { level: 'critical' } ), + unused('ruleExecutionLog.underlyingClient', { level: 'warning' }), ], }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index db05f13702a81..df1a263e1b2c1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -22,7 +22,7 @@ import { ruleRegistryMocks } from '../../../../../../rule_registry/server/mocks' import { siemMock } from '../../../../mocks'; import { createMockConfig } from '../../../../config.mock'; -import { ruleExecutionLogClientMock } from '../../rule_execution_log/__mocks__/rule_execution_log_client'; +import { ruleExecutionLogMock } from '../../rule_execution_log/__mocks__/rule_execution_log_client'; import { requestMock } from './request'; import { internalFrameworkRequest } from '../../../framework'; @@ -56,7 +56,7 @@ export const createMockClients = () => { config: createMockConfig(), appClient: siemMock.createClient(), - ruleExecutionLogClient: ruleExecutionLogClientMock.create(), + ruleExecutionLogClient: ruleExecutionLogMock.client.create(), }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index d186c88e8458e..9118df4fc413f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -23,9 +23,9 @@ import { DETECTION_ENGINE_SIGNALS_FINALIZE_MIGRATION_URL, DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL, DETECTION_ENGINE_RULES_BULK_ACTION, - INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL, + DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL, } from '../../../../../common/constants'; -import { RuleAlertType, HapiReadableStream, IRuleStatusSOAttributes } from '../../rules/types'; +import { RuleAlertType, HapiReadableStream } from '../../rules/types'; import { requestMock } from './request'; import { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema'; import { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema'; @@ -40,10 +40,14 @@ import { getPerformBulkActionSchemaMock, getPerformBulkActionEditSchemaMock, } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { GetCurrentStatusBulkResult } from '../../rule_execution_log/types'; +import { + RuleExecutionEvent, + RuleExecutionStatus, + RuleExecutionSummary, +} from '../../../../../common/detection_engine/schemas/common'; // eslint-disable-next-line no-restricted-imports import type { LegacyRuleNotificationAlertType } from '../../notifications/legacy_types'; +import { RuleExecutionSummariesByRuleId } from '../../rule_execution_log'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], @@ -231,11 +235,13 @@ export const getFindResultWithMultiHits = ({ }; }; -export const internalRuleStatusRequest = () => +export const getRuleExecutionEventsRequest = () => requestMock.create({ - method: 'post', - path: INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL, - body: { ruleId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd' }, + method: 'get', + path: DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL, + params: { + ruleId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + }, }); export const getImportRulesRequest = (hapiStream?: HapiReadableStream) => @@ -473,74 +479,79 @@ export const getMockPrivilegesResult = () => ({ application: {}, }); -export const getEmptySavedObjectsResponse = - (): SavedObjectsFindResponse => ({ - page: 1, - per_page: 1, - total: 0, - saved_objects: [], - }); - -export const getRuleExecutionStatusSucceeded = (): IRuleStatusSOAttributes => ({ - statusDate: '2020-02-18T15:26:49.783Z', - status: RuleExecutionStatus.succeeded, - lastFailureAt: undefined, - lastSuccessAt: '2020-02-18T15:26:49.783Z', - lastFailureMessage: undefined, - lastSuccessMessage: 'succeeded', - lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), - gap: '500.32', - searchAfterTimeDurations: ['200.00'], - bulkCreateTimeDurations: ['800.43'], -}); - -export const getRuleExecutionStatusFailed = (): IRuleStatusSOAttributes => ({ - statusDate: '2020-02-18T15:15:58.806Z', - status: RuleExecutionStatus.failed, - lastFailureAt: '2020-02-18T15:15:58.806Z', - lastSuccessAt: '2020-02-13T20:31:59.855Z', - lastFailureMessage: - 'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.', - lastSuccessMessage: 'succeeded', - lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), - gap: '500.32', - searchAfterTimeDurations: ['200.00'], - bulkCreateTimeDurations: ['800.43'], +export const getEmptySavedObjectsResponse = (): SavedObjectsFindResponse => ({ + page: 1, + per_page: 1, + total: 0, + saved_objects: [], }); -export const getRuleExecutionStatuses = (): IRuleStatusSOAttributes[] => [ - getRuleExecutionStatusSucceeded(), - getRuleExecutionStatusFailed(), -]; - -export const getFindBulkResultStatus = (): GetCurrentStatusBulkResult => ({ - '04128c15-0d1b-4716-a4c5-46997ac7f3bd': { - statusDate: '2020-02-18T15:26:49.783Z', +// TODO: https://github.com/elastic/kibana/pull/121644 clean up +export const getRuleExecutionSummarySucceeded = (): RuleExecutionSummary => ({ + last_execution: { + date: '2020-02-18T15:26:49.783Z', status: RuleExecutionStatus.succeeded, - lastFailureAt: undefined, - lastSuccessAt: '2020-02-18T15:26:49.783Z', - lastFailureMessage: undefined, - lastSuccessMessage: 'succeeded', - lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), - gap: '500.32', - searchAfterTimeDurations: ['200.00'], - bulkCreateTimeDurations: ['800.43'], + status_order: 0, + message: 'succeeded', + metrics: { + total_search_duration_ms: 200, + total_indexing_duration_ms: 800, + execution_gap_duration_s: 500, + }, }, - '1ea5a820-4da1-4e82-92a1-2b43a7bece08': { - statusDate: '2020-02-18T15:15:58.806Z', +}); + +// TODO: https://github.com/elastic/kibana/pull/121644 clean up +export const getRuleExecutionSummaryFailed = (): RuleExecutionSummary => ({ + last_execution: { + date: '2020-02-18T15:15:58.806Z', status: RuleExecutionStatus.failed, - lastFailureAt: '2020-02-18T15:15:58.806Z', - lastSuccessAt: '2020-02-13T20:31:59.855Z', - lastFailureMessage: + status_order: 30, + message: 'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.', - lastSuccessMessage: 'succeeded', - lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), - gap: '500.32', - searchAfterTimeDurations: ['200.00'], - bulkCreateTimeDurations: ['800.43'], + metrics: { + total_search_duration_ms: 200, + total_indexing_duration_ms: 800, + execution_gap_duration_s: 500, + }, }, }); +// TODO: https://github.com/elastic/kibana/pull/121644 clean up +export const getRuleExecutionSummaries = (): RuleExecutionSummariesByRuleId => ({ + '04128c15-0d1b-4716-a4c5-46997ac7f3bd': getRuleExecutionSummarySucceeded(), + '1ea5a820-4da1-4e82-92a1-2b43a7bece08': getRuleExecutionSummaryFailed(), +}); + +// TODO: https://github.com/elastic/kibana/pull/121644 clean up +export const getLastFailures = (): RuleExecutionEvent[] => [ + { + date: '2021-12-28T10:30:00.806Z', + status: RuleExecutionStatus.failed, + message: 'Rule failed', + }, + { + date: '2021-12-28T10:25:00.806Z', + status: RuleExecutionStatus.failed, + message: 'Rule failed', + }, + { + date: '2021-12-28T10:20:00.806Z', + status: RuleExecutionStatus.failed, + message: 'Rule failed', + }, + { + date: '2021-12-28T10:15:00.806Z', + status: RuleExecutionStatus.failed, + message: 'Rule failed', + }, + { + date: '2021-12-28T10:10:00.806Z', + status: RuleExecutionStatus.failed, + message: 'Rule failed', + }, +]; + export const getBasicEmptySearchResponse = (): estypes.SearchResponse => ({ took: 1, timed_out: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 0dcecf3fe3789..2622493a51dc1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -58,10 +58,6 @@ export const getOutputRuleAlertForRest = (): Omit< rule_name_override: undefined, saved_id: undefined, language: 'kuery', - last_failure_at: undefined, - last_failure_message: undefined, - last_success_at: undefined, - last_success_message: undefined, license: 'Elastic License', max_signals: 10000, name: 'Detect Root/Admin Users', @@ -70,8 +66,6 @@ export const getOutputRuleAlertForRest = (): Omit< references: ['http://example.com', 'https://example.com'], severity: 'high', severity_mapping: [], - status: undefined, - status_date: undefined, updated_by: 'elastic', tags: [], throttle: 'no_actions', @@ -95,4 +89,5 @@ export const getOutputRuleAlertForRest = (): Omit< type: 'query', note: '# Investigative notes', version: 1, + execution_summary: undefined, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index 3ec8cb733aa28..119f7a0b07db5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -104,35 +104,12 @@ describe('add_prepackaged_rules_route', () => { }); describe('status codes', () => { - test('returns 200 when creating with a valid actionClient and rulesClient', async () => { + test('returns 200', async () => { const request = addPrepackagedRulesRequest(); const response = await server.inject(request, context); expect(response.status).toEqual(200); }); - - test('returns 404 if rulesClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const request = addPrepackagedRulesRequest(); - const response = await server.inject(request, context); - - expect(response.status).toEqual(404); - expect(response.body).toEqual({ - message: 'Not Found', - status_code: 404, - }); - }); - - test('returns 404 if siem client is unavailable', async () => { - const { securitySolution, ...contextWithoutSecuritySolution } = context; - const response = await server.inject( - addPrepackagedRulesRequest(), - // @ts-expect-error - contextWithoutSecuritySolution - ); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); }); describe('responses', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 50766af669ce7..691548c0a9efd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -54,12 +54,7 @@ export const addPrepackedRulesRoute = (router: SecuritySolutionPluginRouter) => const siemResponse = buildSiemResponse(response); try { - const rulesClient = context.alerting?.getRulesClient(); - const siemClient = context.securitySolution?.getAppClient(); - - if (!siemClient || !rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } + const rulesClient = context.alerting.getRulesClient(); const validated = await createPrepackagedRules( context.securitySolution, @@ -98,7 +93,6 @@ export const createPrepackagedRules = async ( const siemClient = context.getAppClient(); const exceptionsListClient = context.getExceptionListClient() ?? exceptionsClient; const ruleAssetsClient = ruleAssetSavedObjectsClientFactory(savedObjectsClient); - const ruleStatusClient = context.getExecutionLogClient(); const { maxTimelineImportExportSize, @@ -154,7 +148,6 @@ export const createPrepackagedRules = async ( rulesClient, savedObjectsClient, context.getSpaceId(), - ruleStatusClient, rulesToUpdate, signalsIndex, ruleRegistryEnabled diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 6dc303d5a266b..68a3ec0733b60 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -51,25 +51,10 @@ describe.each([ }); describe('status codes', () => { - test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { + test('returns 200', async () => { const response = await server.inject(getReadBulkRequest(), context); expect(response.status).toEqual(200); }); - - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(getReadBulkRequest(), context); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - - test('returns 404 if siem client is unavailable', async () => { - const { securitySolution, ...contextWithoutSecuritySolution } = context; - // @ts-expect-error - const response = await server.inject(getReadBulkRequest(), contextWithoutSecuritySolution); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); }); describe('unhappy paths', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 1db9cca2ca2d8..a5a982a6d78c6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -43,14 +43,10 @@ export const createRulesBulkRoute = ( }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - const rulesClient = context.alerting?.getRulesClient(); + const rulesClient = context.alerting.getRulesClient(); const esClient = context.core.elasticsearch.client; const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.securitySolution?.getAppClient(); - - if (!siemClient || !rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } + const siemClient = context.securitySolution.getAppClient(); const mlAuthz = buildMlAuthz({ license: context.licensing.license, @@ -119,7 +115,7 @@ export const createRulesBulkRoute = ( return transformValidateBulkError( internalRule.params.ruleId, createdRule, - undefined, + null, isRuleRegistryEnabled ); } catch (err) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index a9f5938abb921..449456a12a69c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -10,7 +10,7 @@ import { getEmptyFindResult, getAlertMock, getCreateRequest, - getRuleExecutionStatusSucceeded, + getRuleExecutionSummarySucceeded, getFindResultWithSingleHit, createMlRuleRequest, getBasicEmptySearchResponse, @@ -43,8 +43,8 @@ describe.each([ clients.rulesClient.create.mockResolvedValue( getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) ); // creation succeeds - clients.ruleExecutionLogClient.getCurrentStatus.mockResolvedValue( - getRuleExecutionStatusSucceeded() + clients.ruleExecutionLogClient.getExecutionSummary.mockResolvedValue( + getRuleExecutionSummarySucceeded() ); context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( @@ -53,27 +53,12 @@ describe.each([ createRulesRoute(server.router, ml, isRuleRegistryEnabled); }); - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { + describe('status codes', () => { + test('returns 200 with a rule created via RulesClient', async () => { const response = await server.inject(getCreateRequest(), context); expect(response.status).toEqual(200); }); - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(getCreateRequest(), context); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - - test('returns 404 if siem client is unavailable', async () => { - const { securitySolution, ...contextWithoutSecuritySolution } = context; - // @ts-expect-error - const response = await server.inject(getCreateRequest(), contextWithoutSecuritySolution); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - test('returns 200 if license is not platinum', async () => { (context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 71d453809d0fa..6a87b780f8e75 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -44,15 +44,13 @@ export const createRulesRoute = ( if (validationErrors.length) { return siemResponse.error({ statusCode: 400, body: validationErrors }); } + try { - const rulesClient = context.alerting?.getRulesClient(); + const rulesClient = context.alerting.getRulesClient(); + const ruleExecutionLogClient = context.securitySolution.getExecutionLogClient(); const esClient = context.core.elasticsearch.client; const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.securitySolution?.getAppClient(); - - if (!siemClient || !rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } + const siemClient = context.securitySolution.getAppClient(); if (request.body.rule_id != null) { const rule = await readRules({ @@ -106,13 +104,13 @@ export const createRulesRoute = ( await rulesClient.muteAll({ id: createdRule.id }); } - const ruleStatus = await context.securitySolution.getExecutionLogClient().getCurrentStatus({ - ruleId: createdRule.id, - spaceId: context.securitySolution.getSpaceId(), - }); + const ruleExecutionSummary = await ruleExecutionLogClient.getExecutionSummary( + createdRule.id + ); + const [validated, errors] = newTransformValidate( createdRule, - ruleStatus, + ruleExecutionSummary, isRuleRegistryEnabled ); if (errors != null) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts index 49580fc09ca63..4ac4822c412fa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -82,13 +82,6 @@ describe.each([ ]) ); }); - - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(getDeleteBulkRequest(), context); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); }); describe('request validation', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 149227084ace0..ac8c0fce984b5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -53,14 +53,8 @@ export const deleteRulesBulkRoute = ( }; const handler: Handler = async (context, request, response) => { const siemResponse = buildSiemResponse(response); - - const rulesClient = context.alerting?.getRulesClient(); - - if (!rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } - - const ruleStatusClient = context.securitySolution.getExecutionLogClient(); + const rulesClient = context.alerting.getRulesClient(); + const ruleExecutionLogClient = context.securitySolution.getExecutionLogClient(); const savedObjectsClient = context.core.savedObjects.client; const rules = await Promise.all( @@ -87,19 +81,20 @@ export const deleteRulesBulkRoute = ( return getIdBulkError({ id, ruleId }); } - const ruleStatus = await ruleStatusClient.getCurrentStatus({ - ruleId: migratedRule.id, - spaceId: context.securitySolution.getSpaceId(), - }); + const ruleExecutionSummary = await ruleExecutionLogClient.getExecutionSummary( + migratedRule.id + ); + await deleteRules({ ruleId: migratedRule.id, rulesClient, - ruleStatusClient, + ruleExecutionLogClient, }); + return transformValidateBulkError( idOrRuleIdOrUnknown, migratedRule, - ruleStatus, + ruleExecutionSummary, isRuleRegistryEnabled ); } catch (err) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index 9c126a177eeb5..b18e330eab5a4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -12,7 +12,6 @@ import { getDeleteRequest, getFindResultWithSingleHit, getDeleteRequestById, - getRuleExecutionStatusSucceeded, getEmptySavedObjectsResponse, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; @@ -32,9 +31,6 @@ describe.each([ clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(isRuleRegistryEnabled)); clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); - clients.ruleExecutionLogClient.getCurrentStatus.mockResolvedValue( - getRuleExecutionStatusSucceeded() - ); deleteRulesRoute(server.router, isRuleRegistryEnabled); }); @@ -66,14 +62,6 @@ describe.each([ }); }); - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(getDeleteRequest(), context); - - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - test('catches error if deletion throws error', async () => { clients.rulesClient.delete.mockImplementation(async () => { throw new Error('Test error'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts index 3bb7778e5bc5e..376fa3f569488 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -47,13 +47,10 @@ export const deleteRulesRoute = ( try { const { id, rule_id: ruleId } = request.query; - const rulesClient = context.alerting?.getRulesClient(); + const rulesClient = context.alerting.getRulesClient(); + const ruleExecutionLogClient = context.securitySolution.getExecutionLogClient(); const savedObjectsClient = context.core.savedObjects.client; - if (!rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } - const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const rule = await readRules({ isRuleRegistryEnabled, rulesClient, id, ruleId }); const migratedRule = await legacyMigrate({ rulesClient, @@ -69,17 +66,17 @@ export const deleteRulesRoute = ( }); } - const currentStatus = await ruleStatusClient.getCurrentStatus({ - ruleId: migratedRule.id, - spaceId: context.securitySolution.getSpaceId(), - }); + const ruleExecutionSummary = await ruleExecutionLogClient.getExecutionSummary( + migratedRule.id + ); await deleteRules({ ruleId: migratedRule.id, rulesClient, - ruleStatusClient, + ruleExecutionLogClient, }); - const transformed = transform(migratedRule, currentStatus, isRuleRegistryEnabled); + + const transformed = transform(migratedRule, ruleExecutionSummary, isRuleRegistryEnabled); if (transformed == null) { return siemResponse.error({ statusCode: 500, body: 'failed to transform alert' }); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts index 277590820850b..84b37cd023036 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -45,14 +45,10 @@ export const exportRulesRoute = ( }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - const rulesClient = context.alerting?.getRulesClient(); + const rulesClient = context.alerting.getRulesClient(); const exceptionsClient = context.lists?.getExceptionListClient(); const savedObjectsClient = context.core.savedObjects.client; - if (!rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } - try { const exportSizeLimit = config.maxRuleImportExportSize; if (request.body?.objects != null && request.body.objects.length > exportSizeLimit) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_status_internal_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_status_internal_route.test.ts deleted file mode 100644 index 69f36422aafc5..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_status_internal_route.test.ts +++ /dev/null @@ -1,119 +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 { INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL } from '../../../../../common/constants'; -import { - internalRuleStatusRequest, - getAlertMock, - getRuleExecutionStatusSucceeded, - getRuleExecutionStatusFailed, - resolveAlertMock, -} from '../__mocks__/request_responses'; -import { serverMock, requestContextMock, requestMock } from '../__mocks__'; -import { findRuleStatusInternalRoute } from './find_rule_status_internal_route'; -import { RuleStatusResponse } from '../../rules/types'; -import { AlertExecutionStatusErrorReasons } from '../../../../../../alerting/common'; -import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; - -describe.each([ - ['Legacy', false], - ['RAC', true], -])(`${INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL} - %s`, (_, isRuleRegistryEnabled) => { - let server: ReturnType; - let { clients, context } = requestContextMock.createTools(); - - beforeEach(async () => { - server = serverMock.create(); - ({ clients, context } = requestContextMock.createTools()); - - clients.ruleExecutionLogClient.getCurrentStatus.mockResolvedValue( - getRuleExecutionStatusSucceeded() - ); - clients.ruleExecutionLogClient.getLastFailures.mockResolvedValue([ - getRuleExecutionStatusFailed(), - ]); - clients.rulesClient.get.mockResolvedValue( - getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) - ); - clients.rulesClient.resolve.mockResolvedValue( - resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) - ); - - findRuleStatusInternalRoute(server.router); - }); - - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when finding a single rule status with a valid rulesClient', async () => { - const response = await server.inject(internalRuleStatusRequest(), context); - expect(response.status).toEqual(200); - }); - - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(internalRuleStatusRequest(), context); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - - test('catch error when status search throws error', async () => { - clients.ruleExecutionLogClient.getCurrentStatus.mockImplementation(async () => { - throw new Error('Test error'); - }); - const response = await server.inject(internalRuleStatusRequest(), context); - expect(response.status).toEqual(500); - expect(response.body).toEqual({ - message: 'Test error', - status_code: 500, - }); - }); - - test('returns success if rule status client writes an error status', async () => { - // 0. task manager tried to run the rule but couldn't, so the alerting framework - // wrote an error to the executionStatus. - const failingExecutionRule = resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); - failingExecutionRule.executionStatus = { - status: 'error', - lastExecutionDate: failingExecutionRule.executionStatus.lastExecutionDate, - error: { - reason: AlertExecutionStatusErrorReasons.Read, - message: 'oops', - }, - }; - - // 1. getFailingRules api found a rule where the executionStatus was 'error' - clients.rulesClient.resolve.mockResolvedValue({ - ...failingExecutionRule, - }); - - const request = internalRuleStatusRequest(); - const { ruleId } = request.body; - - const response = await server.inject(request, context); - const responseBody: RuleStatusResponse = response.body; - const ruleStatus = responseBody[ruleId].current_status; - - expect(response.status).toEqual(200); - expect(ruleStatus?.status).toEqual('failed'); - expect(ruleStatus?.last_failure_message).toEqual('Reason: read Message: oops'); - }); - }); - - describe('request validation', () => { - test('disallows singular id query param', async () => { - const request = requestMock.create({ - method: 'post', - path: INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL, - body: { id: ['someId'] }, - }); - const result = server.validate(request); - - expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "undefined" supplied to "ruleId"' - ); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_status_internal_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_status_internal_route.ts deleted file mode 100644 index 6d9b371a9370c..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_status_internal_route.ts +++ /dev/null @@ -1,85 +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 { transformError } from '@kbn/securitysolution-es-utils'; -import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL } from '../../../../../common/constants'; -import { buildSiemResponse, mergeStatuses, getFailingRules } from '../utils'; -import { - findRuleStatusSchema, - FindRuleStatusSchemaDecoded, -} from '../../../../../common/detection_engine/schemas/request/find_rule_statuses_schema'; -import { mergeAlertWithSidecarStatus } from '../../schemas/rule_converters'; - -/** - * Returns the current execution status and metrics + last five failed statuses of a given rule. - * Accepts a rule id. - * - * NOTE: This endpoint is a raw implementation of an endpoint for reading rule execution - * status and logs for a given rule (e.g. for use on the Rule Details page). It will be reworked. - * See the plan in https://github.com/elastic/kibana/pull/115574 - * - * @param router - * @returns RuleStatusResponse containing data only for the given rule (normally it contains data for N rules). - */ -export const findRuleStatusInternalRoute = (router: SecuritySolutionPluginRouter) => { - router.post( - { - path: INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL, - validate: { - body: buildRouteValidation( - findRuleStatusSchema - ), - }, - options: { - tags: ['access:securitySolution'], - }, - }, - async (context, request, response) => { - const { ruleId } = request.body; - - const siemResponse = buildSiemResponse(response); - const rulesClient = context.alerting?.getRulesClient(); - - if (!rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } - - try { - const ruleStatusClient = context.securitySolution.getExecutionLogClient(); - const spaceId = context.securitySolution.getSpaceId(); - - const [currentStatus, lastFailures, failingRules] = await Promise.all([ - ruleStatusClient.getCurrentStatus({ ruleId, spaceId }), - ruleStatusClient.getLastFailures({ ruleId, spaceId }), - getFailingRules([ruleId], rulesClient), - ]); - - const failingRule = failingRules[ruleId]; - let statuses = {}; - - if (currentStatus != null) { - const finalCurrentStatus = - failingRule != null - ? mergeAlertWithSidecarStatus(failingRule, currentStatus) - : currentStatus; - - statuses = mergeStatuses(ruleId, [finalCurrentStatus, ...lastFailures], statuses); - } - - return response.ok({ body: statuses }); - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); - } - } - ); -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index 9f151d1db9292..75698436417c0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -11,10 +11,10 @@ import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; import { requestContextMock, requestMock, serverMock } from '../__mocks__'; import { getAlertMock, - getFindBulkResultStatus, getFindRequest, - getEmptySavedObjectsResponse, getFindResultWithSingleHit, + getEmptySavedObjectsResponse, + getRuleExecutionSummaries, } from '../__mocks__/request_responses'; import { findRulesRoute } from './find_rules_route'; @@ -36,26 +36,19 @@ describe.each([ getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) ); clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); - clients.ruleExecutionLogClient.getCurrentStatusBulk.mockResolvedValue( - getFindBulkResultStatus() + clients.ruleExecutionLogClient.getExecutionSummariesBulk.mockResolvedValue( + getRuleExecutionSummaries() ); findRulesRoute(server.router, logger, isRuleRegistryEnabled); }); - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when finding a single rule with a valid actionClient and alertClient', async () => { + describe('status codes', () => { + test('returns 200', async () => { const response = await server.inject(getFindRequest(), context); expect(response.status).toEqual(200); }); - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(getFindRequest(), context); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - test('catches error if search throws error', async () => { clients.rulesClient.find.mockImplementation(async () => { throw new Error('Test error'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts index 859f84241ae38..1fd6565e77eda 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -18,7 +18,6 @@ import { findRules } from '../../rules/find_rules'; import { buildSiemResponse } from '../utils'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { transformFindAlerts } from './utils'; -import { getCurrentRuleStatuses } from './utils/get_current_rule_statuses'; // eslint-disable-next-line no-restricted-imports import { legacyGetBulkRuleActionsSavedObject } from '../../rule_actions/legacy_get_bulk_rule_actions_saved_object'; @@ -42,6 +41,7 @@ export const findRulesRoute = ( }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); + const validationErrors = findRuleValidateTypeDependents(request.query); if (validationErrors.length) { return siemResponse.error({ statusCode: 400, body: validationErrors }); @@ -49,14 +49,10 @@ export const findRulesRoute = ( try { const { query } = request; - const rulesClient = context.alerting?.getRulesClient(); + const rulesClient = context.alerting.getRulesClient(); + const ruleExecutionLogClient = context.securitySolution.getExecutionLogClient(); const savedObjectsClient = context.core.savedObjects.client; - if (!rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } - - const execLogClient = context.securitySolution.getExecutionLogClient(); const rules = await findRules({ isRuleRegistryEnabled, rulesClient, @@ -67,14 +63,15 @@ export const findRulesRoute = ( filter: query.filter, fields: query.fields, }); + const ruleIds = rules.data.map((rule) => rule.id); - const spaceId = context.securitySolution.getSpaceId(); - const [currentStatusesByRuleId, ruleActions] = await Promise.all([ - getCurrentRuleStatuses({ ruleIds, execLogClient, spaceId, logger }), + const [ruleExecutionSummaries, ruleActions] = await Promise.all([ + ruleExecutionLogClient.getExecutionSummariesBulk(ruleIds), legacyGetBulkRuleActionsSavedObject({ alertIds: ruleIds, savedObjectsClient, logger }), ]); - const transformed = transformFindAlerts(rules, currentStatusesByRuleId, ruleActions); + + const transformed = transformFindAlerts(rules, ruleExecutionSummaries, ruleActions); if (transformed == null) { return siemResponse.error({ statusCode: 500, body: 'Internal error transforming' }); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts index e97744a5fe5a8..3d3d80284aaac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts @@ -93,19 +93,12 @@ describe.each([ ); }); - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when creating a with a valid actionClient and alertClient', async () => { + describe('status codes', () => { + test('returns 200', async () => { const response = await server.inject(getPrepackagedRulesStatusRequest(), context); expect(response.status).toEqual(200); }); - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(getPrepackagedRulesStatusRequest(), context); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - test('catch error when finding rules throws error', async () => { clients.rulesClient.find.mockImplementation(async () => { throw new Error('Test error'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index a18507eea4977..4fcf1bd992c6a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -44,15 +44,11 @@ export const getPrepackagedRulesStatusRoute = ( }, }, async (context, request, response) => { - const savedObjectsClient = context.core.savedObjects.client; const siemResponse = buildSiemResponse(response); - const rulesClient = context.alerting?.getRulesClient(); + const savedObjectsClient = context.core.savedObjects.client; + const rulesClient = context.alerting.getRulesClient(); const ruleAssetsClient = ruleAssetSavedObjectsClientFactory(savedObjectsClient); - if (!rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } - try { const latestPrepackagedRules = await getLatestPrepackagedRules( ruleAssetsClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts new file mode 100644 index 0000000000000..ff4b3ccf812bd --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts @@ -0,0 +1,53 @@ +/* + * 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 { serverMock, requestContextMock } from '../__mocks__'; +import { getRuleExecutionEventsRequest, getLastFailures } from '../__mocks__/request_responses'; +import { getRuleExecutionEventsRoute } from './get_rule_execution_events_route'; + +// TODO: https://github.com/elastic/kibana/pull/121644 clean up +describe('getRuleExecutionEventsRoute', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + + beforeEach(async () => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + + getRuleExecutionEventsRoute(server.router); + }); + + describe('success', () => { + it('returns 200 with found rule execution events', async () => { + const lastFailures = getLastFailures(); + clients.ruleExecutionLogClient.getLastFailures.mockResolvedValue(lastFailures); + + const response = await server.inject(getRuleExecutionEventsRequest(), context); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + events: lastFailures, + }); + }); + }); + + describe('errors', () => { + it('returns 500 when rule execution log client throws an exception', async () => { + clients.ruleExecutionLogClient.getLastFailures.mockImplementation(async () => { + throw new Error('Test error'); + }); + + const response = await server.inject(getRuleExecutionEventsRequest(), context); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Test error', + status_code: 500, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.ts new file mode 100644 index 0000000000000..e449b5b21a298 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.ts @@ -0,0 +1,58 @@ +/* + * 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 { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; +import { buildSiemResponse } from '../utils'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; + +import { DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL } from '../../../../../common/constants'; +import { GetRuleExecutionEventsRequestParams } from '../../../../../common/detection_engine/schemas/request/get_rule_execution_events_request'; +import { GetRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response/get_rule_execution_events_response'; + +// TODO: https://github.com/elastic/kibana/pull/121644 clean up +/** + * Returns execution events of a given rule (e.g. status changes) from Event Log. + * Accepts rule's saved object ID (`rule.id`). + * + * NOTE: This endpoint is under construction. It will be extended and finalized. + * https://github.com/elastic/kibana/issues/119598 + */ +export const getRuleExecutionEventsRoute = (router: SecuritySolutionPluginRouter) => { + router.get( + { + path: DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL, + validate: { + params: buildRouteValidation(GetRuleExecutionEventsRequestParams), + }, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + const { ruleId } = request.params; + const siemResponse = buildSiemResponse(response); + + try { + const executionLog = context.securitySolution.getExecutionLogClient(); + const executionEvents = await executionLog.getLastFailures(ruleId); + + const responseBody: GetRuleExecutionEventsResponse = { + events: executionEvents, + }; + + return response.ok({ body: responseBody }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index 630e7907d4f55..3cb8ff029a68a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -92,7 +92,6 @@ export const importRulesRoute = ( savedObjectsClient, }); - const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const { filename } = (request.body.file as HapiReadableStream).hapi; const fileExtension = extname(filename).toLowerCase(); if (fileExtension !== '.ndjson') { @@ -158,7 +157,6 @@ export const importRulesRoute = ( mlAuthz, overwriteRules: request.query.overwrite, rulesClient, - ruleStatusClient, savedObjectsClient, exceptionsClient, isRuleRegistryEnabled, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index d0d5937eab2d7..6b3fa7ad83c68 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -43,8 +43,8 @@ describe.each([ patchRulesBulkRoute(server.router, ml, isRuleRegistryEnabled); }); - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { + describe('status codes', () => { + test('returns 200', async () => { const response = await server.inject(getPatchBulkRequest(), context); expect(response.status).toEqual(200); }); @@ -88,13 +88,6 @@ describe.each([ ); }); - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(getPatchBulkRequest(), context); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - it('rejects patching a rule to ML if mlAuthz fails', async () => { (buildMlAuthz as jest.Mock).mockReturnValueOnce({ validateRuleType: jest diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 1a79d12ae1b18..f30ba4665aeb0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -46,14 +46,10 @@ export const patchRulesBulkRoute = ( async (context, request, response) => { const siemResponse = buildSiemResponse(response); - const rulesClient = context.alerting?.getRulesClient(); - const ruleStatusClient = context.securitySolution.getExecutionLogClient(); + const rulesClient = context.alerting.getRulesClient(); + const ruleExecutionLogClient = context.securitySolution.getExecutionLogClient(); const savedObjectsClient = context.core.savedObjects.client; - if (!rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } - const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, @@ -193,11 +189,15 @@ export const patchRulesBulkRoute = ( exceptionsList, }); if (rule != null && rule.enabled != null && rule.name != null) { - const ruleStatus = await ruleStatusClient.getCurrentStatus({ - ruleId: rule.id, - spaceId: context.securitySolution.getSpaceId(), - }); - return transformValidateBulkError(rule.id, rule, ruleStatus, isRuleRegistryEnabled); + const ruleExecutionSummary = await ruleExecutionLogClient.getExecutionSummary( + rule.id + ); + return transformValidateBulkError( + rule.id, + rule, + ruleExecutionSummary, + isRuleRegistryEnabled + ); } else { return getIdBulkError({ id, ruleId }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index fe8e4470a61cf..594c91f561204 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -10,7 +10,7 @@ import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../mach import { buildMlAuthz } from '../../../machine_learning/authz'; import { getEmptyFindResult, - getRuleExecutionStatusSucceeded, + getRuleExecutionSummarySucceeded, getAlertMock, getPatchRequest, getFindResultWithSingleHit, @@ -49,18 +49,19 @@ describe.each([ clients.savedObjectsClient.create.mockResolvedValue({ type: 'my-type', id: 'e0b86950-4e9f-11ea-bdbd-07b56aa159b3', - attributes: getRuleExecutionStatusSucceeded(), + // TODO: https://github.com/elastic/kibana/pull/121644 clean up + attributes: getRuleExecutionSummarySucceeded(), references: [], }); // successful transform - clients.ruleExecutionLogClient.getCurrentStatus.mockResolvedValue( - getRuleExecutionStatusSucceeded() + clients.ruleExecutionLogClient.getExecutionSummary.mockResolvedValue( + getRuleExecutionSummarySucceeded() ); patchRulesRoute(server.router, ml, isRuleRegistryEnabled); }); - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { + describe('status codes', () => { + test('returns 200', async () => { const response = await server.inject(getPatchRequest(), context); expect(response.status).toEqual(200); }); @@ -75,13 +76,6 @@ describe.each([ }); }); - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(getPatchRequest(), context); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - test('returns error if requesting a non-rule', async () => { clients.rulesClient.find.mockResolvedValue(nonRuleFindResult(isRuleRegistryEnabled)); const response = await server.inject(getPatchRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts index 6d11fc5851625..f75fcbc4dea28 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -106,14 +106,10 @@ export const patchRulesRoute = ( const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[]; const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; - const rulesClient = context.alerting?.getRulesClient(); - const ruleStatusClient = context.securitySolution.getExecutionLogClient(); + const rulesClient = context.alerting.getRulesClient(); + const ruleExecutionLogClient = context.securitySolution.getExecutionLogClient(); const savedObjectsClient = context.core.savedObjects.client; - if (!rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } - const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, @@ -194,12 +190,13 @@ export const patchRulesRoute = ( exceptionsList, }); if (rule != null && rule.enabled != null && rule.name != null) { - const ruleStatus = await ruleStatusClient.getCurrentStatus({ - ruleId: rule.id, - spaceId: context.securitySolution.getSpaceId(), - }); + const ruleExecutionSummary = await ruleExecutionLogClient.getExecutionSummary(rule.id); - const [validated, errors] = transformValidate(rule, ruleStatus, isRuleRegistryEnabled); + const [validated, errors] = transformValidate( + rule, + ruleExecutionSummary, + isRuleRegistryEnabled + ); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts index c99760b72b56b..4019e519e9db4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts @@ -71,13 +71,6 @@ describe.each([ status_code: 400, }); }); - - it('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(getBulkActionRequest(), context); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); }); describe('rules execution failures', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts index f263cd7b9cec1..6e9d9d0e02d52 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts @@ -158,10 +158,10 @@ export const performBulkActionRoute = ( request.events.completed$.subscribe(() => abortController.abort()); try { - const rulesClient = context.alerting?.getRulesClient(); + const rulesClient = context.alerting.getRulesClient(); + const ruleExecutionLogClient = context.securitySolution.getExecutionLogClient(); const exceptionsClient = context.lists?.getExceptionListClient(); const savedObjectsClient = context.core.savedObjects.client; - const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const mlAuthz = buildMlAuthz({ license: context.licensing.license, @@ -170,10 +170,6 @@ export const performBulkActionRoute = ( savedObjectsClient, }); - if (!rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } - const rules = await findRules({ isRuleRegistryEnabled, rulesClient, @@ -232,7 +228,7 @@ export const performBulkActionRoute = ( await deleteRules({ ruleId: rule.id, rulesClient, - ruleStatusClient, + ruleExecutionLogClient, }); }, abortController.signal diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index b93125d614f12..204c67bf6cf5a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -11,7 +11,7 @@ import { IRuleDataClient } from '../../../../../../rule_registry/server'; import { buildSiemResponse } from '../utils'; import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters'; import { RuleParams } from '../../schemas/rule_schemas'; -import { createWarningsAndErrors } from '../../signals/preview/preview_rule_execution_log_client'; +import { createPreviewRuleExecutionLogger } from '../../signals/preview/preview_rule_execution_logger'; import { parseInterval } from '../../signals/utils'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; @@ -24,7 +24,7 @@ import { previewRulesSchema, RulePreviewLogs, } from '../../../../../common/detection_engine/schemas/request'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; import { AlertInstanceContext, @@ -75,10 +75,7 @@ export const previewRulesRoute = async ( } try { const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.securitySolution?.getAppClient(); - if (!siemClient) { - return siemResponse.error({ statusCode: 404 }); - } + const siemClient = context.securitySolution.getAppClient(); let invocationCount = request.body.invocationCount; if ( @@ -115,14 +112,14 @@ export const previewRulesRoute = async ( const spaceId = siemClient.getSpaceId(); const previewId = uuid.v4(); const username = security?.authc.getCurrentUser(request)?.username; - const { previewRuleExecutionLogClient, warningsAndErrorsStore } = createWarningsAndErrors(); + const previewRuleExecutionLogger = createPreviewRuleExecutionLogger(); const runState: Record = {}; const logs: RulePreviewLogs[] = []; const previewRuleTypeWrapper = createSecurityRuleTypeWrapper({ ...securityRuleTypeOptions, ruleDataClient: previewRuleDataClient, - ruleExecutionLogClientOverride: previewRuleExecutionLogClient, + ruleExecutionLoggerFactory: previewRuleExecutionLogger.factory, }); const runExecutors = async < @@ -194,21 +191,25 @@ export const previewRulesRoute = async ( })) as TState; // Save and reset error and warning logs - const currentLogs = { - errors: warningsAndErrorsStore - .filter((item) => item.newStatus === RuleExecutionStatus.failed) - .map((item) => item.message ?? 'Unkown Error'), - warnings: warningsAndErrorsStore - .filter( - (item) => - item.newStatus === RuleExecutionStatus['partial failure'] || - item.newStatus === RuleExecutionStatus.warning - ) - .map((item) => item.message ?? 'Unknown Warning'), + const errors = previewRuleExecutionLogger.logged.statusChanges + .filter((item) => item.newStatus === RuleExecutionStatus.failed) + .map((item) => item.message ?? 'Unkown Error'); + + const warnings = previewRuleExecutionLogger.logged.statusChanges + .filter( + (item) => + item.newStatus === RuleExecutionStatus['partial failure'] || + item.newStatus === RuleExecutionStatus.warning + ) + .map((item) => item.message ?? 'Unknown Warning'); + + logs.push({ + errors, + warnings, startedAt: startedAt.toDate().toISOString(), - }; - logs.push(currentLogs); - previewRuleExecutionLogClient.clearWarningsAndErrorsStore(); + }); + + previewRuleExecutionLogger.clearLogs(); previousStartedAt = startedAt.toDate(); startedAt.add(parseInterval(internalRule.schedule.interval)); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index 4264ca9961bd4..d462141a8fade 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -16,7 +16,7 @@ import { getFindResultWithSingleHit, nonRuleFindResult, getEmptySavedObjectsResponse, - getRuleExecutionStatusSucceeded, + getRuleExecutionSummarySucceeded, resolveAlertMock, } from '../__mocks__/request_responses'; import { requestMock, requestContextMock, serverMock } from '../__mocks__'; @@ -38,8 +38,8 @@ describe.each([ clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(isRuleRegistryEnabled)); // rule exists clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); // successful transform - clients.ruleExecutionLogClient.getCurrentStatus.mockResolvedValue( - getRuleExecutionStatusSucceeded() + clients.ruleExecutionLogClient.getExecutionSummary.mockResolvedValue( + getRuleExecutionSummarySucceeded() ); clients.rulesClient.resolve.mockResolvedValue({ @@ -51,8 +51,8 @@ describe.each([ readRulesRoute(server.router, logger, isRuleRegistryEnabled); }); - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when reading a single rule with a valid actionClient and alertClient', async () => { + describe('status codes', () => { + test('returns 200', async () => { const response = await server.inject(getReadRequest(), context); expect(response.status).toEqual(200); }); @@ -88,13 +88,6 @@ describe.each([ expect(response.body.alias_target_id).toEqual('myaliastargetid'); }); - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(getReadRequest(), context); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - test('returns error if requesting a non-rule', async () => { clients.rulesClient.find.mockResolvedValue(nonRuleFindResult(isRuleRegistryEnabled)); const response = await server.inject(getReadRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts index 7193387088d84..d327c53089cf6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -48,15 +48,11 @@ export const readRulesRoute = ( const { id, rule_id: ruleId } = request.query; - const rulesClient = context.alerting?.getRulesClient(); - try { - if (!rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } - - const ruleStatusClient = context.securitySolution.getExecutionLogClient(); + const rulesClient = context.alerting.getRulesClient(); + const ruleExecutionLogClient = context.securitySolution.getExecutionLogClient(); const savedObjectsClient = context.core.savedObjects.client; + const rule = await readRules({ id, isRuleRegistryEnabled, @@ -69,14 +65,12 @@ export const readRulesRoute = ( ruleAlertId: rule.id, logger, }); - const currentStatus = await ruleStatusClient.getCurrentStatus({ - ruleId: rule.id, - spaceId: context.securitySolution.getSpaceId(), - }); + + const ruleExecutionSummary = await ruleExecutionLogClient.getExecutionSummary(rule.id); const transformed = transform( rule, - currentStatus, + ruleExecutionSummary, isRuleRegistryEnabled, legacyRuleActions ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 22e8f6543eb7c..88c15f99ed6f7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -46,8 +46,8 @@ describe.each([ updateRulesBulkRoute(server.router, ml, isRuleRegistryEnabled); }); - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { + describe('status codes', () => { + test('returns 200', async () => { const response = await server.inject(getUpdateBulkRequest(), context); expect(response.status).toEqual(200); }); @@ -66,21 +66,6 @@ describe.each([ expect(response.body).toEqual(expected); }); - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(getUpdateBulkRequest(), context); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - - it('returns 404 if siem client is unavailable', async () => { - const { securitySolution, ...contextWithoutSecuritySolution } = context; - // @ts-expect-error - const response = await server.inject(getUpdateBulkRequest(), contextWithoutSecuritySolution); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - test('returns an error if update throws', async () => { clients.rulesClient.update.mockImplementation(() => { throw new Error('Test error'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index e3a125e50bfe9..e560474b8a654 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -40,13 +40,10 @@ export const updateRulesBulkRoute = ( async (context, request, response) => { const siemResponse = buildSiemResponse(response); - const rulesClient = context.alerting?.getRulesClient(); + const rulesClient = context.alerting.getRulesClient(); + const ruleExecutionLogClient = context.securitySolution.getExecutionLogClient(); const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.securitySolution?.getAppClient(); - - if (!siemClient || !rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } + const siemClient = context.securitySolution.getAppClient(); const mlAuthz = buildMlAuthz({ license: context.licensing.license, @@ -55,7 +52,6 @@ export const updateRulesBulkRoute = ( savedObjectsClient, }); - const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const rules = await Promise.all( request.body.map(async (payloadRule) => { const idOrRuleIdOrUnknown = payloadRule.id ?? payloadRule.rule_id ?? '(unknown id)'; @@ -91,11 +87,15 @@ export const updateRulesBulkRoute = ( ruleUpdate: payloadRule, }); if (rule != null) { - const ruleStatus = await ruleStatusClient.getCurrentStatus({ - ruleId: rule.id, - spaceId: context.securitySolution.getSpaceId(), - }); - return transformValidateBulkError(rule.id, rule, ruleStatus, isRuleRegistryEnabled); + const ruleExecutionSummary = await ruleExecutionLogClient.getExecutionSummary( + rule.id + ); + return transformValidateBulkError( + rule.id, + rule, + ruleExecutionSummary, + isRuleRegistryEnabled + ); } else { return getIdBulkError({ id: payloadRule.id, ruleId: payloadRule.rule_id }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 131015880053c..0c1e685a0dad3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -12,7 +12,7 @@ import { getAlertMock, getUpdateRequest, getFindResultWithSingleHit, - getRuleExecutionStatusSucceeded, + getRuleExecutionSummarySucceeded, nonRuleFindResult, typicalMlRulePayload, } from '../__mocks__/request_responses'; @@ -44,16 +44,16 @@ describe.each([ clients.rulesClient.update.mockResolvedValue( getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) ); // successful update - clients.ruleExecutionLogClient.getCurrentStatus.mockResolvedValue( - getRuleExecutionStatusSucceeded() + clients.ruleExecutionLogClient.getExecutionSummary.mockResolvedValue( + getRuleExecutionSummarySucceeded() ); clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index'); updateRulesRoute(server.router, ml, isRuleRegistryEnabled); }); - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { + describe('status codes', () => { + test('returns 200', async () => { const response = await server.inject(getUpdateRequest(), context); expect(response.status).toEqual(200); }); @@ -69,22 +69,6 @@ describe.each([ }); }); - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(getUpdateRequest(), context); - - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - - it('returns 404 if siem client is unavailable', async () => { - const { securitySolution, ...contextWithoutSecuritySolution } = context; - // @ts-expect-error - const response = await server.inject(getUpdateRequest(), contextWithoutSecuritySolution); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - test('returns error when updating non-rule', async () => { clients.rulesClient.find.mockResolvedValue(nonRuleFindResult(isRuleRegistryEnabled)); const response = await server.inject(getUpdateRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index f8bb60eb5f77f..49dcdea0f183c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -44,13 +44,9 @@ export const updateRulesRoute = ( return siemResponse.error({ statusCode: 400, body: validationErrors }); } try { - const rulesClient = context.alerting?.getRulesClient(); + const rulesClient = context.alerting.getRulesClient(); const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.securitySolution?.getAppClient(); - - if (!siemClient || !rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } + const siemClient = context.securitySolution.getAppClient(); const mlAuthz = buildMlAuthz({ license: context.licensing.license, @@ -60,8 +56,6 @@ export const updateRulesRoute = ( }); throwHttpError(await mlAuthz.validateRuleType(request.body.type)); - const ruleStatusClient = context.securitySolution.getExecutionLogClient(); - const existingRule = await readRules({ isRuleRegistryEnabled, rulesClient, @@ -82,11 +76,13 @@ export const updateRulesRoute = ( }); if (rule != null) { - const ruleStatus = await ruleStatusClient.getCurrentStatus({ - ruleId: rule.id, - spaceId: context.securitySolution.getSpaceId(), - }); - const [validated, errors] = transformValidate(rule, ruleStatus, isRuleRegistryEnabled); + const ruleExecutionLogClient = context.securitySolution.getExecutionLogClient(); + const ruleExecutionSummary = await ruleExecutionLogClient.getExecutionSummary(rule.id); + const [validated, errors] = transformValidate( + rule, + ruleExecutionSummary, + isRuleRegistryEnabled + ); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index 8819b068fd5d7..7e9f03cf828a8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -11,24 +11,21 @@ import { Action } from '@kbn/securitysolution-io-ts-alerting-types'; import { SavedObjectsClientContract } from 'kibana/server'; import pMap from 'p-map'; +import { RuleExecutionSummary } from '../../../../../common/detection_engine/schemas/common'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { PartialAlert, FindResult } from '../../../../../../alerting/server'; import { ActionsClient } from '../../../../../../actions/server'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; -import { - RuleAlertType, - isAlertType, - isRuleStatusSavedObjectAttributes, - IRuleStatusSOAttributes, -} from '../../rules/types'; +import { RuleAlertType, isAlertType } from '../../rules/types'; import { createBulkErrorObject, BulkError, OutputError } from '../utils'; import { internalRuleToAPIResponse } from '../../schemas/rule_converters'; import { RuleParams } from '../../schemas/rule_schemas'; import { SanitizedAlert } from '../../../../../../alerting/common'; // eslint-disable-next-line no-restricted-imports import { LegacyRulesActionsSavedObject } from '../../rule_actions/legacy_get_rule_actions_saved_object'; +import { RuleExecutionSummariesByRuleId } from '../../rule_execution_log'; type PromiseFromStreams = ImportRulesSchemaDecoded | Error; const MAX_CONCURRENT_SEARCHES = 10; @@ -99,23 +96,23 @@ export const transformTags = (tags: string[]): string[] => { // Transforms the data but will remove any null or undefined it encounters and not include // those on the export export const transformAlertToRule = ( - alert: SanitizedAlert, - ruleStatus?: IRuleStatusSOAttributes, + rule: SanitizedAlert, + ruleExecutionSummary?: RuleExecutionSummary | null, legacyRuleActions?: LegacyRulesActionsSavedObject | null ): Partial => { - return internalRuleToAPIResponse(alert, ruleStatus, legacyRuleActions); + return internalRuleToAPIResponse(rule, ruleExecutionSummary, legacyRuleActions); }; export const transformAlertsToRules = ( - alerts: RuleAlertType[], + rules: RuleAlertType[], legacyRuleActions: Record ): Array> => { - return alerts.map((alert) => transformAlertToRule(alert, undefined, legacyRuleActions[alert.id])); + return rules.map((rule) => transformAlertToRule(rule, null, legacyRuleActions[rule.id])); }; export const transformFindAlerts = ( - findResults: FindResult, - currentStatusesByRuleId: { [key: string]: IRuleStatusSOAttributes | undefined }, + ruleFindResults: FindResult, + ruleExecutionSummariesByRuleId: RuleExecutionSummariesByRuleId, legacyRuleActions: Record ): { page: number; @@ -124,28 +121,24 @@ export const transformFindAlerts = ( data: Array>; } | null => { return { - page: findResults.page, - perPage: findResults.perPage, - total: findResults.total, - data: findResults.data.map((alert) => { - const status = currentStatusesByRuleId[alert.id]; - return internalRuleToAPIResponse(alert, status, legacyRuleActions[alert.id]); + page: ruleFindResults.page, + perPage: ruleFindResults.perPage, + total: ruleFindResults.total, + data: ruleFindResults.data.map((rule) => { + const executionSummary = ruleExecutionSummariesByRuleId[rule.id]; + return internalRuleToAPIResponse(rule, executionSummary, legacyRuleActions[rule.id]); }), }; }; export const transform = ( - alert: PartialAlert, - ruleStatus?: IRuleStatusSOAttributes, + rule: PartialAlert, + ruleExecutionSummary?: RuleExecutionSummary | null, isRuleRegistryEnabled?: boolean, legacyRuleActions?: LegacyRulesActionsSavedObject | null ): Partial | null => { - if (isAlertType(isRuleRegistryEnabled ?? false, alert)) { - return transformAlertToRule( - alert, - isRuleStatusSavedObjectAttributes(ruleStatus) ? ruleStatus : undefined, - legacyRuleActions - ); + if (isAlertType(isRuleRegistryEnabled ?? false, rule)) { + return transformAlertToRule(rule, ruleExecutionSummary, legacyRuleActions); } return null; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/get_current_rule_statuses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/get_current_rule_statuses.ts deleted file mode 100644 index 4622805e11db6..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/get_current_rule_statuses.ts +++ /dev/null @@ -1,64 +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 { chunk } from 'lodash'; -import { Logger } from 'src/core/server'; -import { initPromisePool } from '../../../../../utils/promise_pool'; -import { GetCurrentStatusBulkResult, IRuleExecutionLogClient } from '../../../rule_execution_log'; - -const RULES_PER_CHUNK = 1000; - -interface GetCurrentRuleStatusesArgs { - ruleIds: string[]; - execLogClient: IRuleExecutionLogClient; - spaceId: string; - logger: Logger; -} - -/** - * Get the most recent execution status for each of the given rule IDs. - * This method splits work into chunks so not to owerwhelm Elasticsearch - * when fetching statuses for a big number of rules. - * - * @param ruleIds Rule IDs to fetch statuses for - * @param execLogClient RuleExecutionLogClient - * @param spaceId Current Space ID - * @param logger Logger - * @returns A dict with rule IDs as keys and rule statuses as values - * - * @throws AggregateError if any of the rule status requests fail - */ -export async function getCurrentRuleStatuses({ - ruleIds, - execLogClient, - spaceId, - logger, -}: GetCurrentRuleStatusesArgs): Promise { - const { results, errors } = await initPromisePool({ - concurrency: 1, - items: chunk(ruleIds, RULES_PER_CHUNK), - executor: (ruleIdsChunk) => - execLogClient - .getCurrentStatusBulk({ - ruleIds: ruleIdsChunk, - spaceId, - }) - .catch((error) => { - logger.error( - `Error fetching rule status: ${error instanceof Error ? error.message : String(error)}` - ); - throw error; - }), - }); - - if (errors.length) { - throw new AggregateError(errors, 'Error fetching rule statuses'); - } - - // Merge all rule statuses into a single dict - return Object.assign({}, ...results); -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.test.ts index c8aad35fa62d5..8d78592dc7fc4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.test.ts @@ -45,7 +45,6 @@ describe('importRules', () => { isRuleRegistryEnabled: true, savedObjectsClient: context.core.savedObjects.client, rulesClient: context.alerting.getRulesClient(), - ruleStatusClient: context.securitySolution.getExecutionLogClient(), exceptionsClient: context.lists?.getExceptionListClient(), spaceId: 'default', signalsIndex: '.signals-index', @@ -64,7 +63,6 @@ describe('importRules', () => { isRuleRegistryEnabled: true, savedObjectsClient: context.core.savedObjects.client, rulesClient: context.alerting.getRulesClient(), - ruleStatusClient: context.securitySolution.getExecutionLogClient(), exceptionsClient: context.lists?.getExceptionListClient(), spaceId: 'default', signalsIndex: '.signals-index', @@ -98,7 +96,6 @@ describe('importRules', () => { isRuleRegistryEnabled: true, savedObjectsClient: context.core.savedObjects.client, rulesClient: context.alerting.getRulesClient(), - ruleStatusClient: context.securitySolution.getExecutionLogClient(), exceptionsClient: context.lists?.getExceptionListClient(), spaceId: 'default', signalsIndex: '.signals-index', @@ -128,7 +125,6 @@ describe('importRules', () => { isRuleRegistryEnabled: true, savedObjectsClient: context.core.savedObjects.client, rulesClient: context.alerting.getRulesClient(), - ruleStatusClient: context.securitySolution.getExecutionLogClient(), exceptionsClient: context.lists?.getExceptionListClient(), spaceId: 'default', signalsIndex: '.signals-index', @@ -163,7 +159,6 @@ describe('importRules', () => { isRuleRegistryEnabled: true, savedObjectsClient: context.core.savedObjects.client, rulesClient: context.alerting.getRulesClient(), - ruleStatusClient: context.securitySolution.getExecutionLogClient(), exceptionsClient: context.lists?.getExceptionListClient(), spaceId: 'default', signalsIndex: '.signals-index', @@ -193,7 +188,6 @@ describe('importRules', () => { isRuleRegistryEnabled: true, savedObjectsClient: context.core.savedObjects.client, rulesClient: context.alerting.getRulesClient(), - ruleStatusClient: context.securitySolution.getExecutionLogClient(), exceptionsClient: context.lists?.getExceptionListClient(), spaceId: 'default', signalsIndex: '.signals-index', @@ -231,7 +225,6 @@ describe('importRules', () => { isRuleRegistryEnabled: true, savedObjectsClient: context.core.savedObjects.client, rulesClient: context.alerting.getRulesClient(), - ruleStatusClient: context.securitySolution.getExecutionLogClient(), exceptionsClient: context.lists?.getExceptionListClient(), spaceId: 'default', signalsIndex: '.signals-index', @@ -268,7 +261,6 @@ describe('importRules', () => { isRuleRegistryEnabled: true, savedObjectsClient: context.core.savedObjects.client, rulesClient: context.alerting.getRulesClient(), - ruleStatusClient: context.securitySolution.getExecutionLogClient(), exceptionsClient: context.lists?.getExceptionListClient(), spaceId: 'default', signalsIndex: '.signals-index', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts index 3f0adaf58a2fd..121e54c768856 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts @@ -23,7 +23,6 @@ import { ImportRulesSchemaDecoded } from '../../../../../../common/detection_eng import { MlAuthz } from '../../../../machine_learning/authz'; import { throwHttpError } from '../../../../machine_learning/validation'; import { RulesClient } from '../../../../../../../../plugins/alerting/server'; -import { IRuleExecutionLogClient } from '../../../rule_execution_log'; import { ExceptionListClient } from '../../../../../../../../plugins/lists/server'; import { checkRuleExceptionReferences } from './check_rule_exception_references'; @@ -45,7 +44,6 @@ export interface RuleExceptionsPromiseFromStreams { * @param isRuleRegistryEnabled {boolean} - feature flag that should be * removed as this is now on and no going back * @param rulesClient {object} - * @param ruleStatusClient {object} * @param savedObjectsClient {object} * @param exceptionsClient {object} * @param spaceId {string} - space being used during import @@ -61,7 +59,6 @@ export const importRules = async ({ overwriteRules, isRuleRegistryEnabled, rulesClient, - ruleStatusClient, savedObjectsClient, exceptionsClient, spaceId, @@ -74,7 +71,6 @@ export const importRules = async ({ overwriteRules: boolean; isRuleRegistryEnabled: boolean; rulesClient: RulesClient; - ruleStatusClient: IRuleExecutionLogClient; savedObjectsClient: SavedObjectsClientContract; exceptionsClient: ExceptionListClient | undefined; spaceId: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts index 032988bcca8be..4d8f64193e6ec 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts @@ -8,11 +8,10 @@ import { transformValidate, transformValidateBulkError } from './validate'; import { BulkError } from '../utils'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; -import { getAlertMock, getRuleExecutionStatusSucceeded } from '../__mocks__/request_responses'; +import { getAlertMock, getRuleExecutionSummarySucceeded } from '../__mocks__/request_responses'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; export const ruleOutput = (): RulesSchema => ({ actions: [], @@ -73,7 +72,7 @@ describe.each([ describe('transformValidate', () => { test('it should do a validation correctly of a partial alert', () => { const ruleAlert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); - const [validated, errors] = transformValidate(ruleAlert, undefined, isRuleRegistryEnabled); + const [validated, errors] = transformValidate(ruleAlert, null, isRuleRegistryEnabled); expect(validated).toEqual(ruleOutput()); expect(errors).toEqual(null); }); @@ -82,7 +81,7 @@ describe.each([ const ruleAlert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); // @ts-expect-error delete ruleAlert.name; - const [validated, errors] = transformValidate(ruleAlert, undefined, isRuleRegistryEnabled); + const [validated, errors] = transformValidate(ruleAlert, null, isRuleRegistryEnabled); expect(validated).toEqual(null); expect(errors).toEqual('Invalid value "undefined" supplied to "name"'); }); @@ -94,7 +93,7 @@ describe.each([ const validatedOrError = transformValidateBulkError( 'rule-1', ruleAlert, - undefined, + null, isRuleRegistryEnabled ); expect(validatedOrError).toEqual(ruleOutput()); @@ -107,7 +106,7 @@ describe.each([ const validatedOrError = transformValidateBulkError( 'rule-1', ruleAlert, - undefined, + null, isRuleRegistryEnabled ); const expected: BulkError = { @@ -120,25 +119,24 @@ describe.each([ expect(validatedOrError).toEqual(expected); }); + // TODO: https://github.com/elastic/kibana/pull/121644 clean up test('it should do a validation correctly of a rule id with ruleStatus passed in', () => { - const ruleStatus = getRuleExecutionStatusSucceeded(); - const ruleAlert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); + const rule = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); + const ruleExecutionSumary = getRuleExecutionSummarySucceeded(); const validatedOrError = transformValidateBulkError( 'rule-1', - ruleAlert, - ruleStatus, + rule, + ruleExecutionSumary, isRuleRegistryEnabled ); const expected: RulesSchema = { ...ruleOutput(), - status: RuleExecutionStatus.succeeded, - status_date: '2020-02-18T15:26:49.783Z', - last_success_at: '2020-02-18T15:26:49.783Z', - last_success_message: 'succeeded', + execution_summary: ruleExecutionSumary, }; expect(validatedOrError).toEqual(expected); }); + // TODO: https://github.com/elastic/kibana/pull/121644 clean up test('it should return error object if "alert" is not expected alert type', () => { const ruleAlert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); // @ts-expect-error @@ -146,7 +144,7 @@ describe.each([ const validatedOrError = transformValidateBulkError( 'rule-1', ruleAlert, - undefined, + null, isRuleRegistryEnabled ); const expected: BulkError = { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index d4bb020cfb672..54a1b3521f2b1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -6,6 +6,8 @@ */ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; + +import { RuleExecutionSummary } from '../../../../../common/detection_engine/schemas/common'; import { FullResponseSchema, fullResponseSchema, @@ -15,11 +17,7 @@ import { rulesSchema, } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { PartialAlert } from '../../../../../../alerting/server'; -import { - isAlertType, - IRuleStatusSOAttributes, - isRuleStatusSavedObjectAttributes, -} from '../../rules/types'; +import { isAlertType } from '../../rules/types'; import { createBulkErrorObject, BulkError } from '../utils'; import { transform, transformAlertToRule } from './utils'; import { RuleParams } from '../../schemas/rule_schemas'; @@ -27,12 +25,17 @@ import { RuleParams } from '../../schemas/rule_schemas'; import { LegacyRulesActionsSavedObject } from '../../rule_actions/legacy_get_rule_actions_saved_object'; export const transformValidate = ( - alert: PartialAlert, - ruleStatus?: IRuleStatusSOAttributes, + rule: PartialAlert, + ruleExecutionSummary: RuleExecutionSummary | null, isRuleRegistryEnabled?: boolean, legacyRuleActions?: LegacyRulesActionsSavedObject | null ): [RulesSchema | null, string | null] => { - const transformed = transform(alert, ruleStatus, isRuleRegistryEnabled, legacyRuleActions); + const transformed = transform( + rule, + ruleExecutionSummary, + isRuleRegistryEnabled, + legacyRuleActions + ); if (transformed == null) { return [null, 'Internal error transforming']; } else { @@ -41,12 +44,17 @@ export const transformValidate = ( }; export const newTransformValidate = ( - alert: PartialAlert, - ruleStatus?: IRuleStatusSOAttributes, + rule: PartialAlert, + ruleExecutionSummary: RuleExecutionSummary | null, isRuleRegistryEnabled?: boolean, legacyRuleActions?: LegacyRulesActionsSavedObject | null ): [FullResponseSchema | null, string | null] => { - const transformed = transform(alert, ruleStatus, isRuleRegistryEnabled, legacyRuleActions); + const transformed = transform( + rule, + ruleExecutionSummary, + isRuleRegistryEnabled, + legacyRuleActions + ); if (transformed == null) { return [null, 'Internal error transforming']; } else { @@ -56,35 +64,21 @@ export const newTransformValidate = ( export const transformValidateBulkError = ( ruleId: string, - alert: PartialAlert, - ruleStatus?: IRuleStatusSOAttributes, + rule: PartialAlert, + ruleExecutionSummary: RuleExecutionSummary | null, isRuleRegistryEnabled?: boolean ): RulesSchema | BulkError => { - if (isAlertType(isRuleRegistryEnabled ?? false, alert)) { - if (ruleStatus && isRuleStatusSavedObjectAttributes(ruleStatus)) { - const transformed = transformAlertToRule(alert, ruleStatus); - const [validated, errors] = validateNonExact(transformed, rulesSchema); - if (errors != null || validated == null) { - return createBulkErrorObject({ - ruleId, - statusCode: 500, - message: errors ?? 'Internal error transforming', - }); - } else { - return validated; - } + if (isAlertType(isRuleRegistryEnabled ?? false, rule)) { + const transformed = transformAlertToRule(rule, ruleExecutionSummary); + const [validated, errors] = validateNonExact(transformed, rulesSchema); + if (errors != null || validated == null) { + return createBulkErrorObject({ + ruleId, + statusCode: 500, + message: errors ?? 'Internal error transforming', + }); } else { - const transformed = transformAlertToRule(alert); - const [validated, errors] = validateNonExact(transformed, rulesSchema); - if (errors != null || validated == null) { - return createBulkErrorObject({ - ruleId, - statusCode: 500, - message: errors ?? 'Internal error transforming', - }); - } else { - return validated; - } + return validated; } } else { return createBulkErrorObject({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts index eee28c0b04149..4e614b5fc8381 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts @@ -5,29 +5,11 @@ * 2.0. */ -import { SavedObjectsFindResponse } from 'kibana/server'; - -import { rulesClientMock } from '../../../../../alerting/server/mocks'; -import { IRuleStatusSOAttributes } from '../rules/types'; import { BadRequestError } from '@kbn/securitysolution-es-utils'; -import { - transformBulkError, - BulkError, - convertToSnakeCase, - SiemResponseFactory, - mergeStatuses, - getFailingRules, -} from './utils'; +import { transformBulkError, BulkError, convertToSnakeCase, SiemResponseFactory } from './utils'; import { responseMock } from './__mocks__'; -import { exampleRuleStatus } from '../signals/__mocks__/es_results'; -import { resolveAlertMock } from './__mocks__/request_responses'; -import { AlertExecutionStatusErrorReasons } from '../../../../../alerting/common'; -import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; -import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; import { CustomHttpRequestError } from '../../../utils/custom_http_request_error'; -let rulesClient: ReturnType; - describe.each([ ['Legacy', false], ['RAC', true], @@ -91,18 +73,19 @@ describe.each([ expect(convertToSnakeCase(values)).toEqual({}); }); it('returns null when passed in undefined', () => { + interface Foo { + bar: Record; + } + // Array accessors can result in undefined but // this is not represented in typescript for some reason, // https://github.com/Microsoft/TypeScript/issues/11122 - const values: SavedObjectsFindResponse = { - page: 0, - per_page: 5, - total: 0, - saved_objects: [], - }; - expect( - convertToSnakeCase(values.saved_objects[0]?.attributes) // this is undefined, but it says it's not - ).toEqual(null); + const array: Foo[] = []; + + // This is undefined, but it says it's not + const undefinedValue = array[0]?.bar; + + expect(convertToSnakeCase(undefinedValue)).toEqual(null); }); }); @@ -132,130 +115,4 @@ describe.each([ ); }); }); - - describe('mergeStatuses', () => { - it('merges statuses and converts from camelCase saved object to snake_case HTTP response', () => { - const statusOne = exampleRuleStatus(); - statusOne.attributes.status = RuleExecutionStatus.failed; - const statusTwo = exampleRuleStatus(); - statusTwo.attributes.status = RuleExecutionStatus.failed; - const currentStatus = exampleRuleStatus(); - const foundRules = [currentStatus.attributes, statusOne.attributes, statusTwo.attributes]; - const res = mergeStatuses(currentStatus.references[0].id, foundRules, { - 'myfakealertid-8cfac': { - current_status: { - status_date: '2020-03-27T22:55:59.517Z', - status: RuleExecutionStatus.succeeded, - last_failure_at: null, - last_success_at: '2020-03-27T22:55:59.517Z', - last_failure_message: null, - last_success_message: 'succeeded', - gap: null, - bulk_create_time_durations: [], - search_after_time_durations: [], - last_look_back_date: null, // NOTE: This is no longer used on the UI, but left here in case users are using it within the API - }, - failures: [], - }, - }); - expect(res).toEqual({ - 'myfakealertid-8cfac': { - current_status: { - status_date: '2020-03-27T22:55:59.517Z', - status: 'succeeded', - last_failure_at: null, - last_success_at: '2020-03-27T22:55:59.517Z', - last_failure_message: null, - last_success_message: 'succeeded', - gap: null, - bulk_create_time_durations: [], - search_after_time_durations: [], - last_look_back_date: null, // NOTE: This is no longer used on the UI, but left here in case users are using it within the API - }, - failures: [], - }, - 'f4b8e31d-cf93-4bde-a265-298bde885cd7': { - current_status: { - status_date: '2020-03-27T22:55:59.517Z', - status: 'succeeded', - last_failure_at: null, - last_success_at: '2020-03-27T22:55:59.517Z', - last_failure_message: null, - last_success_message: 'succeeded', - gap: null, - bulk_create_time_durations: [], - search_after_time_durations: [], - last_look_back_date: null, // NOTE: This is no longer used on the UI, but left here in case users are using it within the API - }, - failures: [ - { - status_date: '2020-03-27T22:55:59.517Z', - status: 'failed', - last_failure_at: null, - last_success_at: '2020-03-27T22:55:59.517Z', - last_failure_message: null, - last_success_message: 'succeeded', - gap: null, - bulk_create_time_durations: [], - search_after_time_durations: [], - last_look_back_date: null, // NOTE: This is no longer used on the UI, but left here in case users are using it within the API - }, - { - status_date: '2020-03-27T22:55:59.517Z', - status: 'failed', - last_failure_at: null, - last_success_at: '2020-03-27T22:55:59.517Z', - last_failure_message: null, - last_success_message: 'succeeded', - gap: null, - bulk_create_time_durations: [], - search_after_time_durations: [], - last_look_back_date: null, // NOTE: This is no longer used on the UI, but left here in case users are using it within the API - }, - ], - }, - }); - }); - }); - - describe('getFailingRules', () => { - beforeEach(() => { - rulesClient = rulesClientMock.create(); - }); - it('getFailingRules finds no failing rules', async () => { - rulesClient.resolve.mockResolvedValue( - resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) - ); - const res = await getFailingRules(['my-fake-id'], rulesClient); - expect(res).toEqual({}); - }); - it('getFailingRules finds a failing rule', async () => { - const foundRule = resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); - foundRule.executionStatus = { - status: 'error', - lastExecutionDate: foundRule.executionStatus.lastExecutionDate, - error: { - reason: AlertExecutionStatusErrorReasons.Read, - message: 'oops', - }, - }; - rulesClient.resolve.mockResolvedValue(foundRule); - const res = await getFailingRules([foundRule.id], rulesClient); - expect(res).toEqual({ [foundRule.id]: foundRule }); - }); - it('getFailingRules throws an error', async () => { - rulesClient.resolve.mockImplementation(() => { - throw new Error('my test error'); - }); - let error; - try { - await getFailingRules(['my-fake-id'], rulesClient); - } catch (exc) { - error = exc; - } - expect(error.message).toEqual( - 'Failed to get executionStatus with RulesClient: my test error' - ); - }); - }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts index a6e977207b838..66418e6f8db24 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts @@ -7,17 +7,13 @@ import { has, snakeCase } from 'lodash/fp'; import { BadRequestError } from '@kbn/securitysolution-es-utils'; -import { SanitizedAlert } from '../../../../../alerting/common'; import { RouteValidationFunction, KibanaResponseFactory, CustomHttpResponseOptions, -} from '../../../../../../../src/core/server'; -import { RulesClient } from '../../../../../alerting/server'; -import { RuleStatusResponse, IRuleStatusSOAttributes } from '../rules/types'; +} from 'kibana/server'; -import { RuleParams } from '../schemas/rule_schemas'; import { CustomHttpRequestError } from '../../../utils/custom_http_request_error'; export interface OutputError { @@ -198,65 +194,3 @@ export const convertToSnakeCase = >( return { ...acc, [newKey]: obj[item] }; }, {}); }; - -/** - * - * @param id rule id - * @param currentStatusAndFailures array of rule statuses where the 0th status is the current status and 1-5 positions are the historical failures - * @param acc accumulated rule id : statuses - */ -export const mergeStatuses = ( - id: string, - currentStatusAndFailures: IRuleStatusSOAttributes[], - acc: RuleStatusResponse -): RuleStatusResponse => { - if (currentStatusAndFailures.length === 0) { - return { - ...acc, - }; - } - const convertedCurrentStatus = convertToSnakeCase( - currentStatusAndFailures[0] - ); - return { - ...acc, - [id]: { - current_status: convertedCurrentStatus, - failures: currentStatusAndFailures - .slice(1) - .map((errorItem) => convertToSnakeCase(errorItem)), - }, - } as RuleStatusResponse; -}; - -export type GetFailingRulesResult = Record>; - -export const getFailingRules = async ( - ids: string[], - rulesClient: RulesClient -): Promise => { - try { - const errorRules = await Promise.all( - ids.map(async (id) => - rulesClient.resolve({ - id, - }) - ) - ); - return errorRules - .filter((rule) => rule.executionStatus.status === 'error') - .reduce((acc, failingRule) => { - return { - [failingRule.id]: { - ...failingRule, - }, - ...acc, - }; - }, {}); - } catch (exc) { - if (exc instanceof CustomHttpRequestError) { - throw exc; - } - throw new Error(`Failed to get executionStatus with RulesClient: ${(exc as Error).message}`); - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts index c01d6afa95190..22a41f356c226 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts @@ -5,23 +5,35 @@ * 2.0. */ -import { IRuleExecutionLogClient } from '../types'; +import { IRuleExecutionLogClient } from '../rule_execution_log_client/client_interface'; +import { + IRuleExecutionLogger, + RuleExecutionContext, +} from '../rule_execution_logger/logger_interface'; -export const ruleExecutionLogClientMock = { +const ruleExecutionLogClientMock = { create: (): jest.Mocked => ({ - find: jest.fn(), - findBulk: jest.fn(), - + getExecutionSummariesBulk: jest.fn(), + getExecutionSummary: jest.fn(), + clearExecutionSummary: jest.fn(), getLastFailures: jest.fn(), - getCurrentStatus: jest.fn(), - getCurrentStatusBulk: jest.fn(), + }), +}; - deleteCurrentStatus: jest.fn(), +const ruleExecutionLoggerMock = { + create: (context: Partial = {}): jest.Mocked => ({ + context: { + ruleId: context.ruleId ?? 'some rule id', + ruleName: context.ruleName ?? 'Some rule', + ruleType: context.ruleType ?? 'some rule type', + spaceId: context.spaceId ?? 'some space id', + }, logStatusChange: jest.fn(), }), }; -export const RuleExecutionLogClient = jest - .fn, []>() - .mockImplementation(ruleExecutionLogClientMock.create); +export const ruleExecutionLogMock = { + client: ruleExecutionLogClientMock, + logger: ruleExecutionLoggerMock, +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts deleted file mode 100644 index d88f4119b691f..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts +++ /dev/null @@ -1,126 +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 { sum } from 'lodash'; -import { SavedObjectsClientContract } from '../../../../../../../../src/core/server'; -import { IEventLogClient, IEventLogService } from '../../../../../../event_log/server'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { IRuleStatusSOAttributes } from '../../rules/types'; -import { SavedObjectsAdapter } from '../saved_objects_adapter/saved_objects_adapter'; -import { - FindBulkExecutionLogArgs, - FindExecutionLogArgs, - GetCurrentStatusArgs, - GetCurrentStatusBulkArgs, - GetCurrentStatusBulkResult, - GetLastFailuresArgs, - IRuleExecutionLogClient, - LogExecutionMetricsArgs, - LogStatusChangeArgs, -} from '../types'; -import { EventLogClient } from './event_log_client'; - -const MAX_LAST_FAILURES = 5; - -export class EventLogAdapter implements IRuleExecutionLogClient { - private eventLogClient: EventLogClient; - /** - * @deprecated Saved objects adapter is used during the transition period while the event log doesn't support all features needed to implement the execution log. - * We use savedObjectsAdapter to write/read the latest rule execution status and eventLogClient to read/write historical execution data. - * We can remove savedObjectsAdapter as soon as the event log supports all methods that we need (find, findBulk). - */ - private savedObjectsAdapter: IRuleExecutionLogClient; - - constructor( - eventLogService: IEventLogService, - eventLogClient: IEventLogClient | undefined, - savedObjectsClient: SavedObjectsClientContract - ) { - this.eventLogClient = new EventLogClient(eventLogService, eventLogClient); - this.savedObjectsAdapter = new SavedObjectsAdapter(savedObjectsClient); - } - - /** @deprecated */ - public async find(args: FindExecutionLogArgs) { - return this.savedObjectsAdapter.find(args); - } - - /** @deprecated */ - public async findBulk(args: FindBulkExecutionLogArgs) { - return this.savedObjectsAdapter.findBulk(args); - } - - public getLastFailures(args: GetLastFailuresArgs): Promise { - const { ruleId } = args; - return this.eventLogClient.getLastStatusChanges({ - ruleId, - count: MAX_LAST_FAILURES, - includeStatuses: [RuleExecutionStatus.failed], - }); - } - - public getCurrentStatus( - args: GetCurrentStatusArgs - ): Promise { - return this.savedObjectsAdapter.getCurrentStatus(args); - } - - public getCurrentStatusBulk(args: GetCurrentStatusBulkArgs): Promise { - return this.savedObjectsAdapter.getCurrentStatusBulk(args); - } - - public async deleteCurrentStatus(ruleId: string): Promise { - await this.savedObjectsAdapter.deleteCurrentStatus(ruleId); - - // EventLog execution events are immutable, nothing to do here - } - - public async logStatusChange(args: LogStatusChangeArgs): Promise { - await Promise.all([ - this.logStatusChangeToSavedObjects(args), - this.logStatusChangeToEventLog(args), - ]); - } - - private async logStatusChangeToSavedObjects(args: LogStatusChangeArgs): Promise { - await this.savedObjectsAdapter.logStatusChange(args); - } - - private async logStatusChangeToEventLog(args: LogStatusChangeArgs): Promise { - if (args.metrics) { - this.logExecutionMetrics({ - ruleId: args.ruleId, - ruleName: args.ruleName, - ruleType: args.ruleType, - spaceId: args.spaceId, - metrics: args.metrics, - }); - } - - this.eventLogClient.logStatusChange(args); - } - - private logExecutionMetrics(args: LogExecutionMetricsArgs): void { - const { ruleId, spaceId, ruleType, ruleName, metrics } = args; - - this.eventLogClient.logExecutionMetrics({ - ruleId, - ruleName, - ruleType, - spaceId, - metrics: { - executionGapDuration: metrics.executionGap?.asSeconds(), - totalIndexingDuration: metrics.indexingDurations - ? sum(metrics.indexingDurations.map(Number)) - : undefined, - totalSearchDuration: metrics.searchDurations - ? sum(metrics.searchDurations.map(Number)) - : undefined, - }, - }); - } -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_client.ts deleted file mode 100644 index 6ce9d3d1c26ee..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_client.ts +++ /dev/null @@ -1,221 +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 { SavedObjectsUtils } from '../../../../../../../../src/core/server'; -import { - IEventLogClient, - IEventLogger, - IEventLogService, - SAVED_OBJECT_REL_PRIMARY, -} from '../../../../../../event_log/server'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { invariant } from '../../../../../common/utils/invariant'; -import { IRuleStatusSOAttributes } from '../../rules/types'; -import { LogStatusChangeArgs } from '../types'; -import { - RuleExecutionLogAction, - RULE_EXECUTION_LOG_PROVIDER, - ALERT_SAVED_OBJECT_TYPE, -} from './constants'; - -const spaceIdToNamespace = SavedObjectsUtils.namespaceStringToId; - -const now = () => new Date().toISOString(); - -const statusSeverityDict: Record = { - [RuleExecutionStatus.succeeded]: 0, - [RuleExecutionStatus['going to run']]: 10, - [RuleExecutionStatus.warning]: 20, - [RuleExecutionStatus['partial failure']]: 20, - [RuleExecutionStatus.failed]: 30, -}; - -interface LogExecutionMetricsArgs { - ruleId: string; - ruleName: string; - ruleType: string; - spaceId: string; - metrics: EventLogExecutionMetrics; -} - -interface EventLogExecutionMetrics { - totalSearchDuration?: number; - totalIndexingDuration?: number; - executionGapDuration?: number; -} - -interface GetLastStatusChangesArgs { - ruleId: string; - count: number; - includeStatuses?: RuleExecutionStatus[]; -} - -interface IExecLogEventLogClient { - getLastStatusChanges(args: GetLastStatusChangesArgs): Promise; - logStatusChange: (args: LogStatusChangeArgs) => void; - logExecutionMetrics: (args: LogExecutionMetricsArgs) => void; -} - -export class EventLogClient implements IExecLogEventLogClient { - private readonly eventLogClient: IEventLogClient | undefined; - private readonly eventLogger: IEventLogger; - private sequence = 0; - - constructor(eventLogService: IEventLogService, eventLogClient: IEventLogClient | undefined) { - this.eventLogClient = eventLogClient; - this.eventLogger = eventLogService.getLogger({ - event: { provider: RULE_EXECUTION_LOG_PROVIDER }, - }); - } - - public async getLastStatusChanges( - args: GetLastStatusChangesArgs - ): Promise { - if (!this.eventLogClient) { - throw new Error('Querying Event Log from a rule executor is not supported at this moment'); - } - - const soType = ALERT_SAVED_OBJECT_TYPE; - const soIds = [args.ruleId]; - const count = args.count; - const includeStatuses = (args.includeStatuses ?? []).map((status) => `"${status}"`); - - const filterBy: string[] = [ - `event.provider: ${RULE_EXECUTION_LOG_PROVIDER}`, - 'event.kind: event', - `event.action: ${RuleExecutionLogAction['status-change']}`, - includeStatuses.length > 0 - ? `kibana.alert.rule.execution.status:${includeStatuses.join(' ')}` - : '', - ]; - - const kqlFilter = filterBy - .filter(Boolean) - .map((item) => `(${item})`) - .join(' and '); - - const findResult = await this.eventLogClient.findEventsBySavedObjectIds(soType, soIds, { - page: 1, - per_page: count, - sort_field: '@timestamp', - sort_order: 'desc', - filter: kqlFilter, - }); - - return findResult.data.map((event) => { - invariant(event, 'Event not found'); - invariant(event['@timestamp'], 'Required "@timestamp" field is not found'); - - const statusDate = event['@timestamp']; - const status = event.kibana?.alert?.rule?.execution?.status as - | RuleExecutionStatus - | undefined; - const isStatusFailed = status === RuleExecutionStatus.failed; - const message = event.message ?? ''; - - return { - statusDate, - status, - lastFailureAt: isStatusFailed ? statusDate : undefined, - lastFailureMessage: isStatusFailed ? message : undefined, - lastSuccessAt: !isStatusFailed ? statusDate : undefined, - lastSuccessMessage: !isStatusFailed ? message : undefined, - lastLookBackDate: undefined, - gap: undefined, - bulkCreateTimeDurations: undefined, - searchAfterTimeDurations: undefined, - }; - }); - } - - public logExecutionMetrics({ - ruleId, - ruleName, - ruleType, - metrics, - spaceId, - }: LogExecutionMetricsArgs) { - this.eventLogger.logEvent({ - '@timestamp': now(), - rule: { - id: ruleId, - name: ruleName, - category: ruleType, - }, - event: { - kind: 'metric', - action: RuleExecutionLogAction['execution-metrics'], - sequence: this.sequence++, - }, - kibana: { - alert: { - rule: { - execution: { - metrics: { - execution_gap_duration_s: metrics.executionGapDuration, - total_search_duration_ms: metrics.totalSearchDuration, - total_indexing_duration_ms: metrics.totalIndexingDuration, - }, - }, - }, - }, - space_ids: [spaceId], - saved_objects: [ - { - rel: SAVED_OBJECT_REL_PRIMARY, - type: ALERT_SAVED_OBJECT_TYPE, - id: ruleId, - namespace: spaceIdToNamespace(spaceId), - }, - ], - }, - }); - } - - public logStatusChange({ - ruleId, - ruleName, - ruleType, - newStatus, - message, - spaceId, - }: LogStatusChangeArgs) { - this.eventLogger.logEvent({ - '@timestamp': now(), - message, - rule: { - id: ruleId, - name: ruleName, - category: ruleType, - }, - event: { - kind: 'event', - action: RuleExecutionLogAction['status-change'], - sequence: this.sequence++, - }, - kibana: { - alert: { - rule: { - execution: { - status: newStatus, - status_order: statusSeverityDict[newStatus], - }, - }, - }, - space_ids: [spaceId], - saved_objects: [ - { - rel: SAVED_OBJECT_REL_PRIMARY, - type: ALERT_SAVED_OBJECT_TYPE, - id: ruleId, - namespace: spaceIdToNamespace(spaceId), - }, - ], - }, - }); - } -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/index.ts index 5c7d9a875056a..cecb8cf70a96d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/index.ts @@ -5,6 +5,16 @@ * 2.0. */ -export * from './rule_execution_log_client'; -export * from './types'; +export { ruleExecutionInfoType } from './rule_execution_info/saved_object'; +export type { + RuleExecutionInfoSavedObject, + RuleExecutionInfoAttributes, +} from './rule_execution_info/saved_object'; + +export * from './rule_execution_log_client/client_interface'; +export * from './rule_execution_logger/logger_interface'; +export * from './rule_execution_log_factory'; + +export { registerEventLogProvider } from './rule_execution_events/register_event_log_provider'; +export { mergeRuleExecutionSummary } from './merge_rule_execution_summary'; export * from './utils/normalization'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/merge_rule_execution_summary.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/merge_rule_execution_summary.ts new file mode 100644 index 0000000000000..e1b7a008ead07 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/merge_rule_execution_summary.ts @@ -0,0 +1,43 @@ +/* + * 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 { + RuleExecutionStatus, + ruleExecutionStatusOrderByStatus, + RuleExecutionSummary, +} from '../../../../common/detection_engine/schemas/common'; +import { RuleAlertType } from '../rules/types'; + +export const mergeRuleExecutionSummary = ( + rule: RuleAlertType, + ruleExecutionSummary: RuleExecutionSummary | null +): RuleExecutionSummary | null => { + if (ruleExecutionSummary == null) { + return null; + } + + const frameworkStatus = rule.executionStatus; + const customStatus = ruleExecutionSummary.last_execution; + + if ( + frameworkStatus.status === 'error' && + new Date(frameworkStatus.lastExecutionDate) > new Date(customStatus.date) + ) { + return { + ...ruleExecutionSummary, + last_execution: { + date: frameworkStatus.lastExecutionDate.toISOString(), + status: RuleExecutionStatus.failed, + status_order: ruleExecutionStatusOrderByStatus[RuleExecutionStatus.failed], + message: `Reason: ${frameworkStatus.error?.reason} Message: ${frameworkStatus.error?.message}`, + metrics: customStatus.metrics, + }, + }; + } + + return ruleExecutionSummary; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/constants.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/constants.ts similarity index 90% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/constants.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/constants.ts index 55624b56e39a0..08bafa92ac02a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/constants.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/constants.ts @@ -5,9 +5,9 @@ * 2.0. */ -export const RULE_EXECUTION_LOG_PROVIDER = 'securitySolution.ruleExecution'; +export const RULE_SAVED_OBJECT_TYPE = 'alert'; -export const ALERT_SAVED_OBJECT_TYPE = 'alert'; +export const RULE_EXECUTION_LOG_PROVIDER = 'securitySolution.ruleExecution'; export enum RuleExecutionLogAction { 'status-change' = 'status-change', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/events_reader.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/events_reader.ts new file mode 100644 index 0000000000000..35c6775f03c05 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/events_reader.ts @@ -0,0 +1,86 @@ +/* + * 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 { Logger } from 'src/core/server'; +import { IEventLogClient } from '../../../../../../event_log/server'; + +import { + RuleExecutionEvent, + RuleExecutionStatus, +} from '../../../../../common/detection_engine/schemas/common'; +import { invariant } from '../../../../../common/utils/invariant'; +import { + RULE_SAVED_OBJECT_TYPE, + RULE_EXECUTION_LOG_PROVIDER, + RuleExecutionLogAction, +} from './constants'; + +export interface IRuleExecutionEventsReader { + getLastStatusChanges(args: GetLastStatusChangesArgs): Promise; +} + +export interface GetLastStatusChangesArgs { + ruleId: string; + count: number; + includeStatuses?: RuleExecutionStatus[]; +} + +export const createRuleExecutionEventsReader = ( + eventLogClient: IEventLogClient, + logger: Logger +): IRuleExecutionEventsReader => { + return { + async getLastStatusChanges(args) { + const soType = RULE_SAVED_OBJECT_TYPE; + const soIds = [args.ruleId]; + const count = args.count; + const includeStatuses = (args.includeStatuses ?? []).map((status) => `"${status}"`); + + const filterBy: string[] = [ + `event.provider: ${RULE_EXECUTION_LOG_PROVIDER}`, + 'event.kind: event', + `event.action: ${RuleExecutionLogAction['status-change']}`, + includeStatuses.length > 0 + ? `kibana.alert.rule.execution.status:${includeStatuses.join(' ')}` + : '', + ]; + + const kqlFilter = filterBy + .filter(Boolean) + .map((item) => `(${item})`) + .join(' and '); + + const findResult = await eventLogClient.findEventsBySavedObjectIds(soType, soIds, { + page: 1, + per_page: count, + sort_field: '@timestamp', + sort_order: 'desc', + filter: kqlFilter, + }); + + return findResult.data.map((event) => { + invariant(event, 'Event not found'); + invariant(event['@timestamp'], 'Required "@timestamp" field is not found'); + invariant( + event.kibana?.alert?.rule?.execution?.status, + 'Required "kibana.alert.rule.execution.status" field is not found' + ); + + const date = event['@timestamp']; + const status = event.kibana?.alert?.rule?.execution?.status as RuleExecutionStatus; + const message = event.message ?? ''; + const result: RuleExecutionEvent = { + date, + status, + message, + }; + + return result; + }); + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/events_writer.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/events_writer.ts new file mode 100644 index 0000000000000..dad30e9cb5d88 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/events_writer.ts @@ -0,0 +1,126 @@ +/* + * 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 { SavedObjectsUtils } from '../../../../../../../../src/core/server'; +import { IEventLogService, SAVED_OBJECT_REL_PRIMARY } from '../../../../../../event_log/server'; +import { + RuleExecutionStatus, + ruleExecutionStatusOrderByStatus, + RuleExecutionMetrics, +} from '../../../../../common/detection_engine/schemas/common'; +import { + RULE_SAVED_OBJECT_TYPE, + RULE_EXECUTION_LOG_PROVIDER, + RuleExecutionLogAction, +} from './constants'; + +export interface IRuleExecutionEventsWriter { + logStatusChange(args: StatusChangeArgs): void; + logExecutionMetrics(args: ExecutionMetricsArgs): void; +} + +export interface BaseArgs { + ruleId: string; + ruleName: string; + ruleType: string; + spaceId: string; +} + +export interface StatusChangeArgs extends BaseArgs { + newStatus: RuleExecutionStatus; + message?: string; +} + +export interface ExecutionMetricsArgs extends BaseArgs { + metrics: RuleExecutionMetrics; +} + +export const createRuleExecutionEventsWriter = ( + eventLogService: IEventLogService +): IRuleExecutionEventsWriter => { + const eventLogger = eventLogService.getLogger({ + event: { provider: RULE_EXECUTION_LOG_PROVIDER }, + }); + + let sequence = 0; + + return { + logStatusChange({ ruleId, ruleName, ruleType, spaceId, newStatus, message }) { + eventLogger.logEvent({ + '@timestamp': nowISO(), + message, + rule: { + id: ruleId, + name: ruleName, + category: ruleType, + }, + event: { + kind: 'event', + action: RuleExecutionLogAction['status-change'], + sequence: sequence++, + }, + kibana: { + alert: { + rule: { + execution: { + status: newStatus, + status_order: ruleExecutionStatusOrderByStatus[newStatus], + }, + }, + }, + space_ids: [spaceId], + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: RULE_SAVED_OBJECT_TYPE, + id: ruleId, + namespace: spaceIdToNamespace(spaceId), + }, + ], + }, + }); + }, + + logExecutionMetrics({ ruleId, ruleName, ruleType, spaceId, metrics }) { + eventLogger.logEvent({ + '@timestamp': nowISO(), + rule: { + id: ruleId, + name: ruleName, + category: ruleType, + }, + event: { + kind: 'metric', + action: RuleExecutionLogAction['execution-metrics'], + sequence: sequence++, + }, + kibana: { + alert: { + rule: { + execution: { + metrics, + }, + }, + }, + space_ids: [spaceId], + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: RULE_SAVED_OBJECT_TYPE, + id: ruleId, + namespace: spaceIdToNamespace(spaceId), + }, + ], + }, + }); + }, + }; +}; + +const nowISO = () => new Date().toISOString(); + +const spaceIdToNamespace = SavedObjectsUtils.namespaceStringToId; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/register_event_log_provider.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/register_event_log_provider.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_info/saved_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_info/saved_object.ts new file mode 100644 index 0000000000000..8a763d59e1a9f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_info/saved_object.ts @@ -0,0 +1,77 @@ +/* + * 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 { SavedObject, SavedObjectsType } from 'src/core/server'; +import { + RuleExecutionMetrics, + RuleExecutionStatus, + RuleExecutionStatusOrder, +} from '../../../../../common/detection_engine/schemas/common'; + +export const RULE_EXECUTION_INFO_TYPE = 'siem-detection-engine-rule-execution-info'; + +/** + * This side-car SO stores information about rule executions (like last status and metrics). + * Eventually we're going to replace it with data stored in the rule itself: + * https://github.com/elastic/kibana/issues/112193 + */ +export type RuleExecutionInfoSavedObject = SavedObject; + +export interface RuleExecutionInfoAttributes { + last_execution: { + date: string; + status: RuleExecutionStatus; + status_order: RuleExecutionStatusOrder; + message: string; + metrics: RuleExecutionMetrics; + }; +} + +const ruleExecutionInfoMappings: SavedObjectsType['mappings'] = { + properties: { + last_execution: { + type: 'object', + properties: { + date: { + type: 'date', + }, + status: { + type: 'keyword', + ignore_above: 1024, + }, + status_order: { + type: 'long', + }, + message: { + type: 'text', + }, + metrics: { + type: 'object', + properties: { + total_search_duration_ms: { + type: 'long', + }, + total_indexing_duration_ms: { + type: 'long', + }, + execution_gap_duration_s: { + type: 'long', + }, + }, + }, + }, + }, + }, +}; + +export const ruleExecutionInfoType: SavedObjectsType = { + name: RULE_EXECUTION_INFO_TYPE, + mappings: ruleExecutionInfoMappings, + hidden: false, + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_info/saved_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_info/saved_objects_client.ts new file mode 100644 index 0000000000000..77bc6c45efd45 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_info/saved_objects_client.ts @@ -0,0 +1,177 @@ +/* + * 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 { + Logger, + SavedObjectsClientContract, + SavedObjectsErrorHelpers, +} from '../../../../../../../../src/core/server'; + +import { + RuleExecutionInfoSavedObject, + RuleExecutionInfoAttributes, + RULE_EXECUTION_INFO_TYPE, +} from './saved_object'; +import { + getRuleExecutionInfoId, + getRuleExecutionInfoReferences, + extractRuleIdFromReferences, +} from './saved_objects_utils'; + +import { ExtMeta } from '../utils/logging'; +import { truncateList } from '../utils/normalization'; + +export interface IRuleExecutionInfoSavedObjectsClient { + createOrUpdate( + ruleId: string, + attributes: RuleExecutionInfoAttributes + ): Promise; + + delete(ruleId: string): Promise; + + getOneByRuleId(ruleId: string): Promise; + + getManyByRuleIds(ruleIds: string[]): Promise; +} + +export type RuleExecutionInfoSavedObjectsByRuleId = Record< + string, + RuleExecutionInfoSavedObject | null +>; + +export const createRuleExecutionInfoSavedObjectsClient = ( + soClient: SavedObjectsClientContract, + logger: Logger +): IRuleExecutionInfoSavedObjectsClient => { + return { + async createOrUpdate(ruleId, attributes) { + try { + const id = getRuleExecutionInfoId(ruleId); + const references = getRuleExecutionInfoReferences(ruleId); + + return await soClient.create( + RULE_EXECUTION_INFO_TYPE, + attributes, + { + id, + references, + overwrite: true, + } + ); + } catch (e) { + const logMessage = 'Error creating/updating rule execution info saved object'; + const logAttributes = `rule id: "${ruleId}"`; + const logReason = e instanceof Error ? e.message : String(e); + const logMeta: ExtMeta = { + rule: { id: ruleId }, + }; + + logger.error(`${logMessage}; ${logAttributes}; ${logReason}`, logMeta); + throw e; + } + }, + + async delete(ruleId) { + try { + const id = getRuleExecutionInfoId(ruleId); + await soClient.delete(RULE_EXECUTION_INFO_TYPE, id); + } catch (e) { + if (SavedObjectsErrorHelpers.isNotFoundError(e)) { + return; + } + + const logMessage = 'Error deleting rule execution info saved object'; + const logAttributes = `rule id: "${ruleId}"`; + const logReason = e instanceof Error ? e.message : String(e); + const logMeta: ExtMeta = { + rule: { id: ruleId }, + }; + + logger.error(`${logMessage}; ${logAttributes}; ${logReason}`, logMeta); + throw e; + } + }, + + async getOneByRuleId(ruleId) { + try { + const id = getRuleExecutionInfoId(ruleId); + return await soClient.get(RULE_EXECUTION_INFO_TYPE, id); + } catch (e) { + if (SavedObjectsErrorHelpers.isNotFoundError(e)) { + return null; + } + + const logMessage = 'Error fetching rule execution info saved object'; + const logAttributes = `rule id: "${ruleId}"`; + const logReason = e instanceof Error ? e.message : String(e); + const logMeta: ExtMeta = { + rule: { id: ruleId }, + }; + + logger.error(`${logMessage}; ${logAttributes}; ${logReason}`, logMeta); + throw e; + } + }, + + async getManyByRuleIds(ruleIds) { + try { + const ids = ruleIds + .map((id) => getRuleExecutionInfoId(id)) + .map((id) => ({ id, type: RULE_EXECUTION_INFO_TYPE })); + + const response = await soClient.bulkGet(ids); + const result = prepopulateRuleExecutionInfoSavedObjectsByRuleId(ruleIds); + + response.saved_objects.forEach((so) => { + // NOTE: We need to explicitly check that this saved object is not an error result and has references in it. + // "Saved object" may not actually contain most of its properties (despite the fact that they are required + // in its TypeScript interface), for example if it wasn't found. In this case it will look like that: + // { + // id: '64b51590-a87e-5afc-9ede-906c3f3483b7', + // type: 'siem-detection-engine-rule-execution-info', + // error: { + // statusCode: 404, + // error: 'Not Found', + // message: 'Saved object [siem-detection-engine-rule-execution-info/64b51590-a87e-5afc-9ede-906c3f3483b7] not found' + // }, + // namespaces: undefined + // } + const hasReferences = !so.error && so.references && Array.isArray(so.references); + const references = hasReferences ? so.references : []; + + const ruleId = extractRuleIdFromReferences(references); + if (ruleId) { + result[ruleId] = so; + } + + if (so.error && so.error.statusCode !== 404) { + logger.error(so.error.message); + } + }); + + return result; + } catch (e) { + const ruleIdsString = `[${truncateList(ruleIds).join(', ')}]`; + + const logMessage = 'Error bulk fetching rule execution info saved objects'; + const logAttributes = `num of rules: ${ruleIds.length}, rule ids: ${ruleIdsString}`; + const logReason = e instanceof Error ? e.message : String(e); + + logger.error(`${logMessage}; ${logAttributes}; ${logReason}`); + throw e; + } + }, + }; +}; + +const prepopulateRuleExecutionInfoSavedObjectsByRuleId = (ruleIds: string[]) => { + const result: RuleExecutionInfoSavedObjectsByRuleId = {}; + ruleIds.forEach((ruleId) => { + result[ruleId] = null; + }); + return result; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_info/saved_objects_utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_info/saved_objects_utils.ts new file mode 100644 index 0000000000000..b0597c178b039 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_info/saved_objects_utils.ts @@ -0,0 +1,36 @@ +/* + * 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 uuidv5 from 'uuid/v5'; +import { SavedObjectReference } from 'src/core/server'; +import { RULE_EXECUTION_INFO_TYPE } from './saved_object'; + +export const getRuleExecutionInfoId = (ruleId: string): string => { + // The uuidv5 namespace constant (uuidv5.DNS) is arbitrary. + return uuidv5(`${RULE_EXECUTION_INFO_TYPE}:${ruleId}`, uuidv5.DNS); +}; + +const RULE_REFERENCE_TYPE = 'alert'; +const RULE_REFERENCE_NAME = 'alert_0'; + +export const getRuleExecutionInfoReferences = (ruleId: string): SavedObjectReference[] => { + return [ + { + id: ruleId, + type: RULE_REFERENCE_TYPE, + name: RULE_REFERENCE_NAME, + }, + ]; +}; + +export const extractRuleIdFromReferences = (references: SavedObjectReference[]): string | null => { + const foundReference = references.find( + (r) => r.type === RULE_REFERENCE_TYPE && r.name === RULE_REFERENCE_NAME + ); + + return foundReference?.id || null; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts deleted file mode 100644 index 3efddf4d7afb0..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts +++ /dev/null @@ -1,131 +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 { Logger, SavedObjectsClientContract } from 'src/core/server'; -import { IEventLogClient, IEventLogService } from '../../../../../event_log/server'; -import { IRuleStatusSOAttributes } from '../rules/types'; -import { EventLogAdapter } from './event_log_adapter/event_log_adapter'; -import { SavedObjectsAdapter } from './saved_objects_adapter/saved_objects_adapter'; -import { - FindBulkExecutionLogArgs, - FindExecutionLogArgs, - IRuleExecutionLogClient, - LogStatusChangeArgs, - UnderlyingLogClient, - GetLastFailuresArgs, - GetCurrentStatusArgs, - GetCurrentStatusBulkArgs, - GetCurrentStatusBulkResult, - ExtMeta, -} from './types'; -import { truncateMessage } from './utils/normalization'; -import { withSecuritySpan } from '../../../utils/with_security_span'; - -interface ConstructorParams { - underlyingClient: UnderlyingLogClient; - savedObjectsClient: SavedObjectsClientContract; - eventLogService: IEventLogService; - eventLogClient?: IEventLogClient; - logger: Logger; -} - -export class RuleExecutionLogClient implements IRuleExecutionLogClient { - private readonly client: IRuleExecutionLogClient; - private readonly logger: Logger; - - constructor(params: ConstructorParams) { - const { underlyingClient, eventLogService, eventLogClient, savedObjectsClient, logger } = - params; - - switch (underlyingClient) { - case UnderlyingLogClient.savedObjects: - this.client = new SavedObjectsAdapter(savedObjectsClient); - break; - case UnderlyingLogClient.eventLog: - this.client = new EventLogAdapter(eventLogService, eventLogClient, savedObjectsClient); - break; - } - - // We write rule execution logs via a child console logger with the context - // "plugins.securitySolution.ruleExecution" - this.logger = logger.get('ruleExecution'); - } - - /** @deprecated */ - public find(args: FindExecutionLogArgs) { - return withSecuritySpan('RuleExecutionLogClient.find', () => this.client.find(args)); - } - - /** @deprecated */ - public findBulk(args: FindBulkExecutionLogArgs) { - return withSecuritySpan('RuleExecutionLogClient.findBulk', () => this.client.findBulk(args)); - } - - public getLastFailures(args: GetLastFailuresArgs): Promise { - return withSecuritySpan('RuleExecutionLogClient.getLastFailures', () => - this.client.getLastFailures(args) - ); - } - - public getCurrentStatus( - args: GetCurrentStatusArgs - ): Promise { - return withSecuritySpan('RuleExecutionLogClient.getCurrentStatus', () => - this.client.getCurrentStatus(args) - ); - } - - public getCurrentStatusBulk(args: GetCurrentStatusBulkArgs): Promise { - return withSecuritySpan('RuleExecutionLogClient.getCurrentStatusBulk', () => - this.client.getCurrentStatusBulk(args) - ); - } - - public async deleteCurrentStatus(ruleId: string): Promise { - await withSecuritySpan('RuleExecutionLogClient.deleteCurrentStatus', () => - this.client.deleteCurrentStatus(ruleId) - ); - } - - public async logStatusChange(args: LogStatusChangeArgs): Promise { - const { newStatus, message, ruleId, ruleName, ruleType, spaceId } = args; - - try { - const truncatedMessage = message ? truncateMessage(message) : message; - await withSecuritySpan( - { - name: 'RuleExecutionLogClient.logStatusChange', - labels: { new_rule_execution_status: args.newStatus }, - }, - () => - this.client.logStatusChange({ - ...args, - message: truncatedMessage, - }) - ); - } catch (e) { - const logMessage = 'Error logging rule execution status change'; - const logAttributes = `status: "${newStatus}", rule id: "${ruleId}", rule name: "${ruleName}"`; - const logReason = e instanceof Error ? `${e.stack}` : `${e}`; - const logMeta: ExtMeta = { - rule: { - id: ruleId, - name: ruleName, - type: ruleType, - execution: { - status: newStatus, - }, - }, - kibana: { - spaceId, - }, - }; - - this.logger.error(`${logMessage}; ${logAttributes}; ${logReason}`, logMeta); - } - } -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client/client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client/client.ts new file mode 100644 index 0000000000000..e87a5d231e0b2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client/client.ts @@ -0,0 +1,47 @@ +/* + * 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 { mapValues } from 'lodash'; +import { Logger } from 'src/core/server'; + +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; + +import { IRuleExecutionEventsReader } from '../rule_execution_events/events_reader'; +import { IRuleExecutionInfoSavedObjectsClient } from '../rule_execution_info/saved_objects_client'; +import { IRuleExecutionLogClient } from './client_interface'; + +const MAX_LAST_FAILURES = 5; + +export const createRuleExecutionLogClient = ( + savedObjectsClient: IRuleExecutionInfoSavedObjectsClient, + eventsReader: IRuleExecutionEventsReader, + logger: Logger +): IRuleExecutionLogClient => { + return { + async getExecutionSummariesBulk(ruleIds) { + const savedObjectsByRuleId = await savedObjectsClient.getManyByRuleIds(ruleIds); + return mapValues(savedObjectsByRuleId, (so) => so?.attributes ?? null); + }, + + async getExecutionSummary(ruleId) { + const savedObject = await savedObjectsClient.getOneByRuleId(ruleId); + return savedObject ? savedObject.attributes : null; + }, + + async clearExecutionSummary(ruleId) { + await savedObjectsClient.delete(ruleId); + }, + + getLastFailures(ruleId) { + return eventsReader.getLastStatusChanges({ + ruleId, + count: MAX_LAST_FAILURES, + includeStatuses: [RuleExecutionStatus.failed], + }); + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client/client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client/client_interface.ts new file mode 100644 index 0000000000000..207cae49d2ab8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client/client_interface.ts @@ -0,0 +1,45 @@ +/* + * 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 { + RuleExecutionEvent, + RuleExecutionSummary, +} from '../../../../../common/detection_engine/schemas/common'; + +/** + * Used from route handlers to fetch and manage various information about the rule execution: + * - execution summary of a rule containing such data as the last status and metrics + * - execution events such as recent failures and status changes + */ +export interface IRuleExecutionLogClient { + /** + * Fetches a list of current execution summaries of multiple rules. + * @param ruleIds A list of saved object ids of multiple rules (`rule.id`). + */ + getExecutionSummariesBulk(ruleIds: string[]): Promise; + + /** + * Fetches current execution summary of a given rule. + * @param ruleId Saved object id of the rule (`rule.id`). + */ + getExecutionSummary(ruleId: string): Promise; + + /** + * Deletes the current execution summary if it exists. + * @param ruleId Saved object id of the rule (`rule.id`). + */ + clearExecutionSummary(ruleId: string): Promise; + + /** + * Fetches last 5 failures (`RuleExecutionStatus.failed`) of a given rule. + * @param ruleId Saved object id of the rule (`rule.id`). + * @deprecated Will be replaced with a more flexible method for fetching execution events. + */ + getLastFailures(ruleId: string): Promise; +} + +export type RuleExecutionSummariesByRuleId = Record; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client/decorator.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client/decorator.ts new file mode 100644 index 0000000000000..070d42354dd68 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client/decorator.ts @@ -0,0 +1,64 @@ +/* + * 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 { chunk } from 'lodash'; +import { Logger } from 'src/core/server'; +import { initPromisePool } from '../../../../utils/promise_pool'; + +import { IRuleExecutionLogClient } from './client_interface'; + +const RULES_PER_CHUNK = 1000; + +export const decorateRuleExecutionLogClient = ( + client: IRuleExecutionLogClient, + logger: Logger +): IRuleExecutionLogClient => { + return { + /** + * Get the current rule execution summary for each of the given rule IDs. + * This method splits work into chunks so not to owerwhelm Elasticsearch + * when fetching statuses for a big number of rules. + * + * @param ruleIds A list of rule IDs (`rule.id`) to fetch summaries for + * @returns A dict with rule IDs as keys and execution summaries as values + * + * @throws AggregateError if any of the rule status requests fail + */ + async getExecutionSummariesBulk(ruleIds) { + const { results, errors } = await initPromisePool({ + concurrency: 1, + items: chunk(ruleIds, RULES_PER_CHUNK), + executor: (ruleIdsChunk) => + client.getExecutionSummariesBulk(ruleIdsChunk).catch((e) => { + logger.error( + `Error fetching rule execution summaries: ${e instanceof Error ? e.stack : String(e)}` + ); + throw e; + }), + }); + + if (errors.length) { + throw new AggregateError(errors, 'Error fetching rule execution summaries'); + } + + // Merge all rule statuses into a single dict + return Object.assign({}, ...results); + }, + + getExecutionSummary(ruleId) { + return client.getExecutionSummary(ruleId); + }, + + clearExecutionSummary(ruleId) { + return client.clearExecutionSummary(ruleId); + }, + + getLastFailures(ruleId) { + return client.getLastFailures(ruleId); + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_factory.ts new file mode 100644 index 0000000000000..958d0d5f6dc34 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_factory.ts @@ -0,0 +1,58 @@ +/* + * 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 { Logger, SavedObjectsClientContract } from 'src/core/server'; +import { IEventLogClient, IEventLogService } from '../../../../../event_log/server'; + +import { IRuleExecutionLogClient } from './rule_execution_log_client/client_interface'; +import { createRuleExecutionLogClient } from './rule_execution_log_client/client'; +import { decorateRuleExecutionLogClient } from './rule_execution_log_client/decorator'; + +import { + IRuleExecutionLogger, + RuleExecutionContext, +} from './rule_execution_logger/logger_interface'; +import { createRuleExecutionLogger } from './rule_execution_logger/logger'; + +import { createRuleExecutionEventsReader } from './rule_execution_events/events_reader'; +import { createRuleExecutionEventsWriter } from './rule_execution_events/events_writer'; +import { createRuleExecutionInfoSavedObjectsClient } from './rule_execution_info/saved_objects_client'; + +export type RuleExecutionLogClientFactory = ( + savedObjectsClient: SavedObjectsClientContract, + eventLogClient: IEventLogClient, + logger: Logger +) => IRuleExecutionLogClient; + +export const ruleExecutionLogClientFactory: RuleExecutionLogClientFactory = ( + savedObjectsClient, + eventLogClient, + logger +) => { + const soClient = createRuleExecutionInfoSavedObjectsClient(savedObjectsClient, logger); + const eventsReader = createRuleExecutionEventsReader(eventLogClient, logger); + const executionLogClient = createRuleExecutionLogClient(soClient, eventsReader, logger); + return decorateRuleExecutionLogClient(executionLogClient, logger); +}; + +export type RuleExecutionLoggerFactory = ( + savedObjectsClient: SavedObjectsClientContract, + eventLogService: IEventLogService, + logger: Logger, + context: RuleExecutionContext +) => IRuleExecutionLogger; + +export const ruleExecutionLoggerFactory: RuleExecutionLoggerFactory = ( + savedObjectsClient, + eventLogService, + logger, + context +) => { + const soClient = createRuleExecutionInfoSavedObjectsClient(savedObjectsClient, logger); + const eventsWriter = createRuleExecutionEventsWriter(eventLogService); + return createRuleExecutionLogger(soClient, eventsWriter, logger, context); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger.ts new file mode 100644 index 0000000000000..f67aae472ef60 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger.ts @@ -0,0 +1,145 @@ +/* + * 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 { sum } from 'lodash'; +import { Duration } from 'moment'; +import { Logger } from 'src/core/server'; + +import { + RuleExecutionStatus, + ruleExecutionStatusOrderByStatus, + RuleExecutionMetrics, +} from '../../../../../common/detection_engine/schemas/common'; + +import { ExtMeta } from '../utils/logging'; +import { truncateValue } from '../utils/normalization'; + +import { IRuleExecutionEventsWriter } from '../rule_execution_events/events_writer'; +import { IRuleExecutionInfoSavedObjectsClient } from '../rule_execution_info/saved_objects_client'; +import { IRuleExecutionLogger, RuleExecutionContext, StatusChangeArgs } from './logger_interface'; + +export const createRuleExecutionLogger = ( + savedObjectsClient: IRuleExecutionInfoSavedObjectsClient, + eventsWriter: IRuleExecutionEventsWriter, + logger: Logger, + context: RuleExecutionContext +): IRuleExecutionLogger => { + const { ruleId, ruleName, ruleType, spaceId } = context; + + const ruleExecutionLogger: IRuleExecutionLogger = { + get context() { + return context; + }, + + async logStatusChange(args) { + try { + const normalizedArgs = normalizeStatusChangeArgs(args); + await Promise.all([ + writeStatusChangeToSavedObjects(normalizedArgs), + writeStatusChangeToEventLog(normalizedArgs), + ]); + } catch (e) { + const logMessage = 'Error logging rule execution status change'; + const logAttributes = `status: "${args.newStatus}", rule id: "${ruleId}", rule name: "${ruleName}"`; + const logReason = e instanceof Error ? e.stack ?? e.message : String(e); + const logMeta: ExtMeta = { + rule: { + id: ruleId, + name: ruleName, + type: ruleType, + execution: { + status: args.newStatus, + }, + }, + kibana: { + spaceId, + }, + }; + + logger.error(`${logMessage}; ${logAttributes}; ${logReason}`, logMeta); + } + }, + }; + + const writeStatusChangeToSavedObjects = async ( + args: NormalizedStatusChangeArgs + ): Promise => { + const { newStatus, message, metrics } = args; + + await savedObjectsClient.createOrUpdate(ruleId, { + last_execution: { + date: nowISO(), + status: newStatus, + status_order: ruleExecutionStatusOrderByStatus[newStatus], + message, + metrics: metrics ?? {}, + }, + }); + }; + + const writeStatusChangeToEventLog = (args: NormalizedStatusChangeArgs): void => { + const { newStatus, message, metrics } = args; + + if (metrics) { + eventsWriter.logExecutionMetrics({ + ruleId, + ruleName, + ruleType, + spaceId, + metrics, + }); + } + + eventsWriter.logStatusChange({ + ruleId, + ruleName, + ruleType, + spaceId, + newStatus, + message, + }); + }; + + return ruleExecutionLogger; +}; + +const nowISO = () => new Date().toISOString(); + +interface NormalizedStatusChangeArgs { + newStatus: RuleExecutionStatus; + message: string; + metrics?: RuleExecutionMetrics; +} + +const normalizeStatusChangeArgs = (args: StatusChangeArgs): NormalizedStatusChangeArgs => { + const { newStatus, message, metrics } = args; + + return { + newStatus, + message: truncateValue(message) ?? '', + metrics: metrics + ? { + total_search_duration_ms: normalizeDurations(metrics.searchDurations), + total_indexing_duration_ms: normalizeDurations(metrics.indexingDurations), + execution_gap_duration_s: normalizeGap(metrics.executionGap), + } + : undefined, + }; +}; + +const normalizeDurations = (durations?: string[]): number | undefined => { + if (durations == null) { + return undefined; + } + + const sumAsFloat = sum(durations.map(Number)); + return Math.round(sumAsFloat); +}; + +const normalizeGap = (duration?: Duration): number | undefined => { + return duration ? Math.round(duration.asSeconds()) : undefined; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger_interface.ts new file mode 100644 index 0000000000000..e31c10bd9747f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger_interface.ts @@ -0,0 +1,48 @@ +/* + * 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 { Duration } from 'moment'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; + +/** + * Used from rule executors to log various information about the rule execution: + * - rule status changes + * - rule execution metrics + * - later - generic messages and any kind of info we'd need to log for rule + * monitoring or debugging purposes + */ +export interface IRuleExecutionLogger { + context: RuleExecutionContext; + + /** + * Writes information about new rule statuses and measured execution metrics: + * 1. To .kibana-* index as a custom `siem-detection-engine-rule-execution-info` saved object. + * This SO is used for fast access to last execution info of a large amount of rules. + * 2. To .kibana-event-log-* index in order to track history of rule executions. + * @param args Information about the status change event. + */ + logStatusChange(args: StatusChangeArgs): Promise; +} + +export interface RuleExecutionContext { + ruleId: string; + ruleName: string; + ruleType: string; + spaceId: string; +} + +export interface StatusChangeArgs { + newStatus: RuleExecutionStatus; + message?: string; + metrics?: MetricsArgs; +} + +export interface MetricsArgs { + searchDurations?: string[]; + indexingDurations?: string[]; + executionGap?: Duration; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts deleted file mode 100644 index 5ec17e47a8b31..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts +++ /dev/null @@ -1,139 +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 { - SavedObject, - SavedObjectsClientContract, - SavedObjectsCreateOptions, - SavedObjectsFindOptions, - SavedObjectsFindOptionsReference, - SavedObjectsFindResult, - SavedObjectsUpdateResponse, -} from 'kibana/server'; -import { get } from 'lodash'; -import { withSecuritySpan } from '../../../../utils/with_security_span'; -// eslint-disable-next-line no-restricted-imports -import { legacyRuleStatusSavedObjectType } from '../../rules/legacy_rule_status/legacy_rule_status_saved_object_mappings'; -import { IRuleStatusSOAttributes } from '../../rules/types'; - -export interface RuleStatusSavedObjectsClient { - find: ( - options: Omit & { ruleId: string } - ) => Promise>>; - findBulk: (ids: string[], statusesPerId: number) => Promise; - create: ( - attributes: IRuleStatusSOAttributes, - options: SavedObjectsCreateOptions - ) => Promise>; - update: ( - id: string, - attributes: Partial, - options: SavedObjectsCreateOptions - ) => Promise>; - delete: (id: string) => Promise<{}>; -} - -export interface FindBulkResponse { - [key: string]: IRuleStatusSOAttributes[] | undefined; -} - -/** - * @deprecated Use RuleExecutionLogClient instead - */ -export const ruleStatusSavedObjectsClientFactory = ( - savedObjectsClient: SavedObjectsClientContract -): RuleStatusSavedObjectsClient => ({ - find: async (options) => { - return withSecuritySpan('RuleStatusSavedObjectsClient.find', async () => { - const references = { - id: options.ruleId, - type: 'alert', - }; - const result = await savedObjectsClient.find({ - ...options, - type: legacyRuleStatusSavedObjectType, - hasReference: references, - }); - return result.saved_objects; - }); - }, - findBulk: async (ids, statusesPerId) => { - if (ids.length === 0) { - return {}; - } - return withSecuritySpan('RuleStatusSavedObjectsClient.findBulk', async () => { - const references = ids.map((alertId) => ({ - id: alertId, - type: 'alert', - })); - const order: 'desc' = 'desc'; - // NOTE: Once https://github.com/elastic/kibana/issues/115153 is resolved - // ${legacyRuleStatusSavedObjectType}.statusDate will need to be updated to - // ${legacyRuleStatusSavedObjectType}.attributes.statusDate - const aggs = { - references: { - nested: { - path: `${legacyRuleStatusSavedObjectType}.references`, - }, - aggs: { - alertIds: { - terms: { - field: `${legacyRuleStatusSavedObjectType}.references.id`, - size: ids.length, - }, - aggs: { - rule_status: { - reverse_nested: {}, - aggs: { - most_recent_statuses: { - top_hits: { - sort: [ - { - [`${legacyRuleStatusSavedObjectType}.statusDate`]: { - order, - }, - }, - ], - size: statusesPerId, - }, - }, - }, - }, - }, - }, - }, - }, - }; - const results = await savedObjectsClient.find({ - hasReference: references, - aggs, - type: legacyRuleStatusSavedObjectType, - perPage: 0, - }); - const buckets = get(results, 'aggregations.references.alertIds.buckets'); - return buckets.reduce((acc: Record, bucket: unknown) => { - const key = get(bucket, 'key'); - const hits = get(bucket, 'rule_status.most_recent_statuses.hits.hits'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - acc[key] = hits.map((hit: any) => hit._source[legacyRuleStatusSavedObjectType]); - return acc; - }, {}); - }); - }, - create: (attributes, options) => - withSecuritySpan('RuleStatusSavedObjectsClient.create', () => - savedObjectsClient.create(legacyRuleStatusSavedObjectType, attributes, options) - ), - update: (id, attributes, options) => - withSecuritySpan('RuleStatusSavedObjectsClient.update', () => - savedObjectsClient.update(legacyRuleStatusSavedObjectType, id, attributes, options) - ), - delete: (id) => - withSecuritySpan('RuleStatusSavedObjectsClient.delete', () => - savedObjectsClient.delete(legacyRuleStatusSavedObjectType, id) - ), -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts deleted file mode 100644 index 230306c453ace..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts +++ /dev/null @@ -1,191 +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 { mapValues } from 'lodash'; -import { SavedObject, SavedObjectReference } from 'src/core/server'; -import { SavedObjectsClientContract } from '../../../../../../../../src/core/server'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; -// eslint-disable-next-line no-restricted-imports -import { legacyGetRuleReference } from '../../rules/legacy_rule_status/legacy_utils'; -import { IRuleStatusSOAttributes } from '../../rules/types'; -import { - ExecutionMetrics, - FindBulkExecutionLogArgs, - FindExecutionLogArgs, - GetCurrentStatusArgs, - GetCurrentStatusBulkArgs, - GetCurrentStatusBulkResult, - GetLastFailuresArgs, - IRuleExecutionLogClient, - LogStatusChangeArgs, -} from '../types'; -import { - RuleStatusSavedObjectsClient, - ruleStatusSavedObjectsClientFactory, -} from './rule_status_saved_objects_client'; - -const MAX_ERRORS = 5; -// 1st is mutable status, followed by 5 most recent failures -const MAX_RULE_STATUSES = 1 + MAX_ERRORS; - -const convertMetricFields = ( - metrics: ExecutionMetrics -): Pick< - IRuleStatusSOAttributes, - 'gap' | 'searchAfterTimeDurations' | 'bulkCreateTimeDurations' -> => ({ - gap: metrics.executionGap?.humanize(), - searchAfterTimeDurations: metrics.searchDurations, - bulkCreateTimeDurations: metrics.indexingDurations, -}); - -export class SavedObjectsAdapter implements IRuleExecutionLogClient { - private ruleStatusClient: RuleStatusSavedObjectsClient; - - constructor(savedObjectsClient: SavedObjectsClientContract) { - this.ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); - } - - private findRuleStatusSavedObjects(ruleId: string, count: number) { - return this.ruleStatusClient.find({ - perPage: count, - sortField: 'statusDate', - sortOrder: 'desc', - ruleId, - }); - } - - /** @deprecated */ - public find({ ruleId, logsCount = 1 }: FindExecutionLogArgs) { - return this.findRuleStatusSavedObjects(ruleId, logsCount); - } - - /** @deprecated */ - public findBulk({ ruleIds, logsCount = 1 }: FindBulkExecutionLogArgs) { - return this.ruleStatusClient.findBulk(ruleIds, logsCount); - } - - public async getLastFailures(args: GetLastFailuresArgs): Promise { - const result = await this.findRuleStatusSavedObjects(args.ruleId, MAX_RULE_STATUSES); - - // The first status is always the current one followed by 5 last failures. - // We skip the current status and return only the failures. - return result.map((so) => so.attributes).slice(1); - } - - public async getCurrentStatus( - args: GetCurrentStatusArgs - ): Promise { - const result = await this.findRuleStatusSavedObjects(args.ruleId, 1); - const currentStatusSavedObject = result[0]; - return currentStatusSavedObject?.attributes; - } - - public async getCurrentStatusBulk( - args: GetCurrentStatusBulkArgs - ): Promise { - const { ruleIds } = args; - const result = await this.ruleStatusClient.findBulk(ruleIds, 1); - return mapValues(result, (attributes = []) => attributes[0]); - } - - public async deleteCurrentStatus(ruleId: string): Promise { - const statusSavedObjects = await this.findRuleStatusSavedObjects(ruleId, MAX_RULE_STATUSES); - await Promise.all(statusSavedObjects.map((so) => this.ruleStatusClient.delete(so.id))); - } - - private findRuleStatuses = async ( - ruleId: string - ): Promise>> => - this.findRuleStatusSavedObjects(ruleId, MAX_RULE_STATUSES); - - public async logStatusChange({ newStatus, ruleId, message, metrics }: LogStatusChangeArgs) { - const references: SavedObjectReference[] = [legacyGetRuleReference(ruleId)]; - const ruleStatuses = await this.findRuleStatuses(ruleId); - const [currentStatus] = ruleStatuses; - const attributes = buildRuleStatusAttributes({ - status: newStatus, - message, - metrics, - currentAttributes: currentStatus?.attributes, - }); - // Create or update current status - if (currentStatus) { - await this.ruleStatusClient.update(currentStatus.id, attributes, { references }); - } else { - await this.ruleStatusClient.create(attributes, { references }); - } - - if (newStatus === RuleExecutionStatus.failed) { - await Promise.all([ - // Persist the current failure in the last five errors list - this.ruleStatusClient.create(attributes, { references }), - // Drop oldest failures - ...ruleStatuses - .slice(MAX_RULE_STATUSES - 1) - .map((status) => this.ruleStatusClient.delete(status.id)), - ]); - } - } -} - -const defaultStatusAttributes: IRuleStatusSOAttributes = { - statusDate: '', - status: RuleExecutionStatus['going to run'], - lastFailureAt: null, - lastSuccessAt: null, - lastFailureMessage: null, - lastSuccessMessage: null, - gap: null, - bulkCreateTimeDurations: [], - searchAfterTimeDurations: [], - lastLookBackDate: null, -}; - -const buildRuleStatusAttributes = ({ - status, - message, - metrics = {}, - currentAttributes, -}: { - status: RuleExecutionStatus; - message?: string; - metrics?: ExecutionMetrics; - currentAttributes?: IRuleStatusSOAttributes; -}): IRuleStatusSOAttributes => { - const now = new Date().toISOString(); - const baseAttributes: IRuleStatusSOAttributes = { - ...defaultStatusAttributes, - ...currentAttributes, - statusDate: now, - status: - status === RuleExecutionStatus.warning ? RuleExecutionStatus['partial failure'] : status, - ...convertMetricFields(metrics), - }; - - switch (status) { - case RuleExecutionStatus.succeeded: - case RuleExecutionStatus.warning: - case RuleExecutionStatus['partial failure']: { - return { - ...baseAttributes, - lastSuccessAt: now, - lastSuccessMessage: message, - }; - } - case RuleExecutionStatus.failed: { - return { - ...baseAttributes, - lastFailureAt: now, - lastFailureMessage: message, - }; - } - case RuleExecutionStatus['going to run']: { - return baseAttributes; - } - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts deleted file mode 100644 index 1fa256b0f088c..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts +++ /dev/null @@ -1,120 +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 { Duration } from 'moment'; -import { LogMeta, SavedObjectsFindResult } from 'src/core/server'; -import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; -import { IRuleStatusSOAttributes } from '../rules/types'; - -export enum UnderlyingLogClient { - 'savedObjects' = 'savedObjects', - 'eventLog' = 'eventLog', -} - -export interface IRuleExecutionLogClient { - /** @deprecated */ - find(args: FindExecutionLogArgs): Promise>>; - /** @deprecated */ - findBulk(args: FindBulkExecutionLogArgs): Promise; - - getLastFailures(args: GetLastFailuresArgs): Promise; - getCurrentStatus(args: GetCurrentStatusArgs): Promise; - getCurrentStatusBulk(args: GetCurrentStatusBulkArgs): Promise; - - deleteCurrentStatus(ruleId: string): Promise; - - logStatusChange(args: LogStatusChangeArgs): Promise; -} - -/** @deprecated */ -export interface FindExecutionLogArgs { - ruleId: string; - spaceId: string; - logsCount?: number; -} - -/** @deprecated */ -export interface FindBulkExecutionLogArgs { - ruleIds: string[]; - spaceId: string; - logsCount?: number; -} - -/** @deprecated */ -export interface FindBulkExecutionLogResponse { - [ruleId: string]: IRuleStatusSOAttributes[] | undefined; -} - -export interface GetLastFailuresArgs { - ruleId: string; - spaceId: string; -} - -export interface GetCurrentStatusArgs { - ruleId: string; - spaceId: string; -} - -export interface GetCurrentStatusBulkArgs { - ruleIds: string[]; - spaceId: string; -} - -export interface GetCurrentStatusBulkResult { - [ruleId: string]: IRuleStatusSOAttributes; -} - -export interface CreateExecutionLogArgs { - attributes: IRuleStatusSOAttributes; - spaceId: string; -} - -export interface LogStatusChangeArgs { - ruleId: string; - ruleName: string; - ruleType: string; - spaceId: string; - newStatus: RuleExecutionStatus; - message?: string; - /** - * @deprecated Use RuleExecutionLogClient.logExecutionMetrics to write metrics instead - */ - metrics?: ExecutionMetrics; -} - -export interface LogExecutionMetricsArgs { - ruleId: string; - ruleName: string; - ruleType: string; - spaceId: string; - metrics: ExecutionMetrics; -} - -export interface ExecutionMetrics { - searchDurations?: string[]; - indexingDurations?: string[]; - /** - * @deprecated lastLookBackDate is logged only by SavedObjectsAdapter and should be removed in the future - */ - lastLookBackDate?: string; - executionGap?: Duration; -} - -/** - * Custom extended log metadata that rule execution logger can attach to every log record. - */ -export type ExtMeta = LogMeta & { - rule?: LogMeta['rule'] & { - type?: string; - execution?: { - status?: RuleExecutionStatus; - }; - }; - kibana?: { - spaceId?: string; - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/utils/logging.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/utils/logging.ts new file mode 100644 index 0000000000000..4d373f68c00bc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/utils/logging.ts @@ -0,0 +1,24 @@ +/* + * 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 { LogMeta } from 'src/core/server'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; + +/** + * Custom extended log metadata that rule execution logger can attach to every log record. + */ +export type ExtMeta = LogMeta & { + rule?: LogMeta['rule'] & { + type?: string; + execution?: { + status?: RuleExecutionStatus; + }; + }; + kibana?: { + spaceId?: string; + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/utils/normalization.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/utils/normalization.ts index baaee9446eee3..e24f631811168 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/utils/normalization.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/utils/normalization.ts @@ -7,28 +7,32 @@ import { take, toString, truncate, uniq } from 'lodash'; -// When we write rule execution status updates to `siem-detection-engine-rule-status` saved objects -// or to event log, we write success and failure messages as well. Those messages are built from -// N errors collected during the "big loop" in the Detection Engine, where N can be very large. -// When N is large the resulting message strings are so large that these documents are up to 26MB. -// These large documents may cause migrations to fail because a batch of 1000 documents easily +// When we write rule execution status updates to saved objects or to event log, +// we can write warning/failure messages as well. In some cases those messages +// are built from N errors collected during the "big loop" of Detection Engine, +// where N can be a large number. When N is large the resulting message strings +// can take ~26MB of memory and make the resulting documents huge. These large +// documents may cause migrations to fail because a batch of 1000 documents can // exceed Elasticsearch's `http.max_content_length` which defaults to 100mb. -// In order to fix that, we need to truncate those messages to an adequate MAX length. +// In order to fix that, we need to truncate messages to an adequate MAX length. // https://github.com/elastic/kibana/pull/112257 -const MAX_MESSAGE_LENGTH = 10240; +const MAX_STRING_LENGTH = 10240; const MAX_LIST_LENGTH = 20; -export const truncateMessage = (value: unknown): string | undefined => { +export const truncateValue = ( + value: unknown, + maxLength = MAX_STRING_LENGTH +): string | undefined => { if (value === undefined) { return value; } const str = toString(value); - return truncate(str, { length: MAX_MESSAGE_LENGTH }); + return truncate(str, { length: maxLength }); }; -export const truncateMessageList = (list: string[]): string[] => { +export const truncateList = (list: T[], maxLength = MAX_LIST_LENGTH): T[] => { const deduplicatedList = uniq(list); - return take(deduplicatedList, MAX_LIST_LENGTH); + return take(deduplicatedList, maxLength); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/threshold.ts index 19b1405cb1433..334add447dcbe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/threshold.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/threshold.ts @@ -77,6 +77,7 @@ export const mockThresholdResults = { }, }; +// TODO: https://github.com/elastic/kibana/pull/121644 clean up export const sampleThresholdAlert = { _id: 'b3ad77a4-65bd-4c4e-89cf-13c46f54bc4d', _index: 'some-index', @@ -155,10 +156,6 @@ export const sampleThresholdAlert = { type: 'query', threat: [], version: 1, - status: 'succeeded', - status_date: '2020-02-22T16:47:50.047Z', - last_success_at: '2020-02-22T16:47:50.047Z', - last_success_message: 'succeeded', max_signals: 100, language: 'kuery', rule_id: 'f88a544c-1d4e-4652-ae2a-c953b38da5d0', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 50d553045b5d5..a643f428e58e7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -35,8 +35,8 @@ import { import { getNotificationResultsLink } from '../notifications/utils'; import { createResultObject } from './utils'; import { bulkCreateFactory, wrapHitsFactory, wrapSequencesFactory } from './factories'; -import { RuleExecutionLogClient, truncateMessageList } from '../rule_execution_log'; -import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; +import { truncateList } from '../rule_execution_log'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common'; import { scheduleThrottledNotificationActions } from '../notifications/schedule_throttle_notification_actions'; import aadFieldConversion from '../routes/index/signal_aad_mapping.json'; import { extractReferences, injectReferences } from '../signals/saved_object_references'; @@ -44,7 +44,7 @@ import { withSecuritySpan } from '../../../utils/with_security_span'; /* eslint-disable complexity */ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = - ({ lists, logger, config, ruleDataClient, eventLogService, ruleExecutionLogClientOverride }) => + ({ lists, logger, config, ruleDataClient, eventLogService, ruleExecutionLoggerFactory }) => (type) => { const { alertIgnoreFields: ignoreFields, alertMergeStrategy: mergeStrategy } = config; const persistenceRuleType = createPersistenceRuleTypeWrapper({ ruleDataClient, logger }); @@ -76,14 +76,17 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = const esClient = scopedClusterClient.asCurrentUser; - const ruleStatusClient = ruleExecutionLogClientOverride - ? ruleExecutionLogClientOverride - : new RuleExecutionLogClient({ - underlyingClient: config.ruleExecutionLog.underlyingClient, - savedObjectsClient, - eventLogService, - logger, - }); + const ruleExecutionLogger = ruleExecutionLoggerFactory( + savedObjectsClient, + eventLogService, + logger, + { + ruleId: alertId, + ruleName: rule.name, + ruleType: rule.ruleTypeId, + spaceId, + } + ); const completeRule = { ruleConfig: rule, @@ -95,7 +98,6 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = actions, name, schedule: { interval }, - ruleTypeId, } = completeRule.ruleConfig; const refresh = actions.length ? 'wait_for' : false; @@ -111,14 +113,8 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = logger.debug(buildRuleMessage(`interval: ${interval}`)); let wroteWarningStatus = false; - const basicLogArguments = { - spaceId, - ruleId: alertId, - ruleName: name, - ruleType: ruleTypeId, - }; - await ruleStatusClient.logStatusChange({ - ...basicLogArguments, + + await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus['going to run'], }); @@ -152,11 +148,10 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = const privileges = await checkPrivilegesFromEsClient(esClient, inputIndices); wroteWarningStatus = await hasReadIndexPrivileges({ - ...basicLogArguments, privileges, logger, buildRuleMessage, - ruleStatusClient, + ruleExecutionLogger, }); if (!wroteWarningStatus) { @@ -170,13 +165,12 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = }) ); wroteWarningStatus = await hasTimestampFields({ - ...basicLogArguments, timestampField: hasTimestampOverride ? (timestampOverride as string) : '@timestamp', timestampFieldCapsResponse: timestampFieldCaps, inputIndices, - ruleStatusClient, + ruleExecutionLogger, logger, buildRuleMessage, }); @@ -185,10 +179,9 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = } catch (exc) { const errorMessage = buildRuleMessage(`Check privileges failed to execute ${exc}`); logger.warn(errorMessage); - await ruleStatusClient.logStatusChange({ - ...basicLogArguments, - message: errorMessage, + await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus['partial failure'], + message: errorMessage, }); wroteWarningStatus = true; } @@ -212,8 +205,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ); logger.warn(gapMessage); hasError = true; - await ruleStatusClient.logStatusChange({ - ...basicLogArguments, + await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus.failed, message: gapMessage, metrics: { executionGap: remainingGap }, @@ -293,11 +285,8 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = } if (result.warningMessages.length) { - const warningMessage = buildRuleMessage( - truncateMessageList(result.warningMessages).join() - ); - await ruleStatusClient.logStatusChange({ - ...basicLogArguments, + const warningMessage = buildRuleMessage(truncateList(result.warningMessages).join()); + await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus['partial failure'], message: warningMessage, }); @@ -356,14 +345,12 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ); if (!hasError && !wroteWarningStatus && !result.warning) { - await ruleStatusClient.logStatusChange({ - ...basicLogArguments, + await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus.succeeded, message: 'succeeded', metrics: { - indexingDurations: result.bulkCreateTimes, searchDurations: result.searchAfterTimes, - lastLookBackDate: result.lastLookbackDate?.toISOString(), + indexingDurations: result.bulkCreateTimes, }, }); } @@ -397,17 +384,15 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = } const errorMessage = buildRuleMessage( 'Bulk Indexing of signals failed:', - truncateMessageList(result.errors).join() + truncateList(result.errors).join() ); logger.error(errorMessage); - await ruleStatusClient.logStatusChange({ - ...basicLogArguments, + await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus.failed, message: errorMessage, metrics: { - indexingDurations: result.bulkCreateTimes, searchDurations: result.searchAfterTimes, - lastLookBackDate: result.lastLookbackDate?.toISOString(), + indexingDurations: result.bulkCreateTimes, }, }); } @@ -437,14 +422,12 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ); logger.error(message); - await ruleStatusClient.logStatusChange({ - ...basicLogArguments, + await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus.failed, message, metrics: { - indexingDurations: result.bulkCreateTimes, searchDurations: result.searchAfterTimes, - lastLookBackDate: result.lastLookbackDate?.toISOString(), + indexingDurations: result.bulkCreateTimes, }, }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts index bfa99f68b48c3..4a33dd0868aab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts @@ -14,6 +14,7 @@ import { createRuleTypeMocks } from '../__mocks__/rule_type'; import { createSecurityRuleTypeWrapper } from '../create_security_rule_type_wrapper'; import { createMockConfig } from '../../routes/__mocks__'; import { createMockTelemetryEventsSender } from '../../../telemetry/__mocks__'; +import { ruleExecutionLogMock } from '../../rule_execution_log/__mocks__/rule_execution_log_client'; import { sampleDocNoSortId } from '../../signals/__mocks__/es_results'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; @@ -29,8 +30,6 @@ jest.mock('../utils/get_list_client', () => ({ }), })); -jest.mock('../../rule_execution_log/rule_execution_log_client'); - describe('Custom Query Alerts', () => { const mocks = createRuleTypeMocks(); const { dependencies, executor, services } = mocks; @@ -41,6 +40,7 @@ describe('Custom Query Alerts', () => { config: createMockConfig(), ruleDataClient, eventLogService, + ruleExecutionLoggerFactory: () => ruleExecutionLogMock.logger.create(), }); const eventsTelemetry = createMockTelemetryEventsSender(true); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index 17e749ec251b4..9aae815a4188a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -38,7 +38,7 @@ import { ExperimentalFeatures } from '../../../../common/experimental_features'; import { IEventLogService } from '../../../../../event_log/server'; import { AlertsFieldMap, RulesFieldMap } from '../../../../common/field_maps'; import { TelemetryEventsSender } from '../../telemetry/sender'; -import { IRuleExecutionLogClient } from '../rule_execution_log'; +import { RuleExecutionLoggerFactory } from '../rule_execution_log'; import { commonParamsCamelToSnake } from '../schemas/rule_converters'; export interface SecurityAlertTypeReturnValue { @@ -99,7 +99,7 @@ export interface CreateSecurityRuleTypeWrapperProps { config: ConfigType; ruleDataClient: IRuleDataClient; eventLogService: IEventLogService; - ruleExecutionLogClientOverride?: IRuleExecutionLogClient; + ruleExecutionLoggerFactory: RuleExecutionLoggerFactory; } export type CreateSecurityRuleTypeWrapper = ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts index 42d7f960beb22..0fada9327cb3a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts @@ -6,29 +6,29 @@ */ import { rulesClientMock } from '../../../../../alerting/server/mocks'; -import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; +import { ruleExecutionLogMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; import { deleteRules } from './delete_rules'; import { DeleteRuleOptions } from './types'; describe('deleteRules', () => { let rulesClient: ReturnType; - let ruleStatusClient: ReturnType; + let ruleExecutionLogClient: ReturnType; beforeEach(() => { rulesClient = rulesClientMock.create(); - ruleStatusClient = ruleExecutionLogClientMock.create(); + ruleExecutionLogClient = ruleExecutionLogMock.client.create(); }); it('should delete the rule along with its actions, and statuses', async () => { const options: DeleteRuleOptions = { ruleId: 'ruleId', rulesClient, - ruleStatusClient, + ruleExecutionLogClient, }; await deleteRules(options); expect(rulesClient.delete).toHaveBeenCalledWith({ id: options.ruleId }); - expect(ruleStatusClient.deleteCurrentStatus).toHaveBeenCalledWith(options.ruleId); + expect(ruleExecutionLogClient.clearExecutionSummary).toHaveBeenCalledWith(options.ruleId); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts index 880132434f7cc..cbe6f36914663 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts @@ -7,7 +7,11 @@ import { DeleteRuleOptions } from './types'; -export const deleteRules = async ({ ruleId, rulesClient, ruleStatusClient }: DeleteRuleOptions) => { +export const deleteRules = async ({ + ruleId, + rulesClient, + ruleExecutionLogClient, +}: DeleteRuleOptions) => { await rulesClient.delete({ id: ruleId }); - await ruleStatusClient.deleteCurrentStatus(ruleId); + await ruleExecutionLogClient.clearExecutionSummary(ruleId); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 0e07ac4e56a9f..a047100bc5262 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -187,10 +187,6 @@ describe.each([ interval: '5m', rule_id: 'rule-1', language: 'kuery', - last_failure_at: undefined, - last_failure_message: undefined, - last_success_at: undefined, - last_success_message: undefined, license: 'Elastic License', output_index: '.siem-signals', max_signals: 10000, @@ -198,8 +194,6 @@ describe.each([ risk_score_mapping: [], rule_name_override: undefined, saved_id: undefined, - status: undefined, - status_date: undefined, name: 'Detect Root/Admin Users', query: 'user.name: root or user.name: admin', references: ['http://example.com', 'https://example.com'], @@ -217,6 +211,7 @@ describe.each([ note: '# Investigative notes', version: 1, exceptions_list: getListArrayMock(), + execution_summary: undefined, }, ], }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts index b8f1467cffe87..7381e2c3a1f02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -127,7 +127,7 @@ export const getRulesFromObjects = async ( ) { return { statusCode: 200, - rule: transformAlertToRule(matchingRule, undefined, legacyActions[matchingRule.id]), + rule: transformAlertToRule(matchingRule, null, legacyActions[matchingRule.id]), }; } else { return { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts deleted file mode 100644 index 120b8cf20b18d..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts +++ /dev/null @@ -1,110 +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 { - SavedObjectMigrationFn, - SavedObjectReference, - SavedObjectSanitizedDoc, - SavedObjectUnsanitizedDoc, -} from 'kibana/server'; -import { isString } from 'lodash/fp'; -import { truncateMessage } from '../../rule_execution_log'; -import { IRuleStatusSOAttributes } from '../types'; -// eslint-disable-next-line no-restricted-imports -import { legacyGetRuleReference } from './legacy_utils'; - -export const truncateMessageFields: SavedObjectMigrationFn> = (doc) => { - const { lastFailureMessage, lastSuccessMessage, ...otherAttributes } = doc.attributes; - - return { - ...doc, - attributes: { - ...otherAttributes, - lastFailureMessage: truncateMessage(lastFailureMessage), - lastSuccessMessage: truncateMessage(lastSuccessMessage), - }, - references: doc.references ?? [], - }; -}; - -/** - * This migrates alertId within legacy `siem-detection-engine-rule-status` to saved object references on an upgrade. - * We only migrate alertId if we find these conditions: - * - alertId is a string and not null, undefined, or malformed data. - * - The existing references do not already have a alertId found within it. - * - * Some of these issues could crop up during either user manual errors of modifying things, earlier migration - * issues, etc... so we are safer to check them as possibilities - * - * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) - * @param doc The document having an alertId to migrate into references - * @returns The document migrated with saved object references - */ -export const legacyMigrateRuleAlertIdSOReferences = ( - doc: SavedObjectUnsanitizedDoc -): SavedObjectSanitizedDoc => { - const { alertId, ...otherAttributes } = doc.attributes; - const existingReferences = doc.references ?? []; - - // early return if alertId is not a string as expected, still removing alertId as the mapping no longer exists - if (!isString(alertId)) { - return { - ...doc, - attributes: otherAttributes, - references: existingReferences, - }; - } - - const alertReferences = legacyMigrateAlertId({ - alertId, - existingReferences, - }); - - return { - ...doc, - attributes: otherAttributes, - references: [...existingReferences, ...alertReferences], - }; -}; - -/** - * This is a helper to migrate "alertId" - * - * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) - * - * @param existingReferences The existing saved object references - * @param alertId The alertId to migrate - * - * @returns The savedObjectReferences migrated - */ -export const legacyMigrateAlertId = ({ - existingReferences, - alertId, -}: { - existingReferences: SavedObjectReference[]; - alertId: string; -}): SavedObjectReference[] => { - const existingReferenceFound = existingReferences.find((reference) => { - return reference.id === alertId && reference.type === 'alert'; - }); - if (existingReferenceFound) { - return []; - } else { - return [legacyGetRuleReference(alertId)]; - } -}; - -/** - * This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and - * additional fields on the Alerting Framework Rule SO. - * - * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) - */ -export const legacyRuleStatusSavedObjectMigration = { - '7.15.2': truncateMessageFields, - '7.16.0': legacyMigrateRuleAlertIdSOReferences, -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts deleted file mode 100644 index 298c75b8b7d51..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts +++ /dev/null @@ -1,74 +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 { SavedObjectsType } from 'kibana/server'; -// eslint-disable-next-line no-restricted-imports -import { legacyRuleStatusSavedObjectMigration } from './legacy_migrations'; - -/** - * This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and - * additional fields on the Alerting Framework Rule SO. - * - * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) - */ -export const legacyRuleStatusSavedObjectType = 'siem-detection-engine-rule-status'; - -/** - * This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and - * additional fields on the Alerting Framework Rule SO. - * - * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) - */ -export const ruleStatusSavedObjectMappings: SavedObjectsType['mappings'] = { - properties: { - status: { - type: 'keyword', - }, - statusDate: { - type: 'date', - }, - lastFailureAt: { - type: 'date', - }, - lastSuccessAt: { - type: 'date', - }, - lastFailureMessage: { - type: 'text', - }, - lastSuccessMessage: { - type: 'text', - }, - lastLookBackDate: { - type: 'date', - }, - gap: { - type: 'text', - }, - bulkCreateTimeDurations: { - type: 'float', - }, - searchAfterTimeDurations: { - type: 'float', - }, - }, -}; - -/** - * This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and - * additional fields on the Alerting Framework Rule SO. - * - * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) - */ -export const legacyRuleStatusType: SavedObjectsType = { - convertToMultiNamespaceTypeVersion: '8.0.0', - name: legacyRuleStatusSavedObjectType, - hidden: false, - namespaceType: 'multiple-isolated', - mappings: ruleStatusSavedObjectMappings, - migrations: legacyRuleStatusSavedObjectMigration, -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_utils.ts deleted file mode 100644 index 62de5ce591230..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_utils.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. - */ - -/** - * Given an id this returns a legacy rule reference. - * @param id The id of the alert - * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) - */ -export const legacyGetRuleReference = (id: string) => ({ - id, - type: 'alert', - name: 'alert_0', -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index e2ea5aefaea1a..79406c30e1572 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { get } from 'lodash/fp'; import { Readable } from 'stream'; -import { SavedObject, SavedObjectAttributes, SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectAttributes, SavedObjectsClientContract } from 'kibana/server'; import type { MachineLearningJobIdOrUndefined, From, @@ -84,12 +83,6 @@ import { QueryFilterOrUndefined, FieldsOrUndefined, SortOrderOrUndefined, - RuleExecutionStatus, - LastSuccessAt, - StatusDate, - LastSuccessMessage, - LastFailureAt, - LastFailureMessage, Author, AuthorOrUndefined, LicenseOrUndefined, @@ -98,65 +91,16 @@ import { RuleNameOverrideOrUndefined, EventCategoryOverrideOrUndefined, NamespaceOrUndefined, -} from '../../../../common/detection_engine/schemas/common/schemas'; +} from '../../../../common/detection_engine/schemas/common'; import { RulesClient, PartialAlert } from '../../../../../alerting/server'; import { SanitizedAlert } from '../../../../../alerting/common'; import { PartialFilter } from '../types'; import { RuleParams } from '../schemas/rule_schemas'; -import { IRuleExecutionLogClient } from '../rule_execution_log/types'; +import { IRuleExecutionLogClient } from '../rule_execution_log'; export type RuleAlertType = SanitizedAlert; -export interface IRuleStatusSOAttributes extends SavedObjectAttributes { - statusDate: StatusDate; - lastFailureAt: LastFailureAt | null | undefined; - lastFailureMessage: LastFailureMessage | null | undefined; - lastSuccessAt: LastSuccessAt | null | undefined; - lastSuccessMessage: LastSuccessMessage | null | undefined; - status: RuleExecutionStatus | null | undefined; - lastLookBackDate: string | null | undefined; - gap: string | null | undefined; - bulkCreateTimeDurations: string[] | null | undefined; - searchAfterTimeDurations: string[] | null | undefined; -} - -export interface IRuleStatusResponseAttributes { - status_date: StatusDate; - last_failure_at: LastFailureAt | null | undefined; - last_failure_message: LastFailureMessage | null | undefined; - last_success_at: LastSuccessAt | null | undefined; - last_success_message: LastSuccessMessage | null | undefined; - status: RuleExecutionStatus | null | undefined; - last_look_back_date: string | null | undefined; // NOTE: This is no longer used on the UI, but left here in case users are using it within the API - gap: string | null | undefined; - bulk_create_time_durations: string[] | null | undefined; - search_after_time_durations: string[] | null | undefined; -} - -export interface RuleStatusResponse { - [key: string]: { - current_status: IRuleStatusResponseAttributes | null | undefined; - failures: IRuleStatusResponseAttributes[] | null | undefined; - }; -} - -export interface IRuleStatusSavedObject { - type: string; - id: string; - attributes: Array>; - references: unknown[]; - updated_at: string; - version: string; -} - -export interface IRuleStatusFindType { - page: number; - per_page: number; - total: number; - saved_objects: IRuleStatusSavedObject[]; -} - // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface IRuleAssetSOAttributes extends Record { rule_id: string | null | undefined; @@ -197,10 +141,6 @@ export const isAlertType = ( : partialAlert.alertTypeId === SIGNALS_ID; }; -export const isRuleStatusSavedObjectAttributes = (obj: unknown): obj is IRuleStatusSOAttributes => { - return get('status', obj) != null; -}; - export interface CreateRulesOptions { rulesClient: RulesClient; anomalyThreshold: AnomalyThresholdOrUndefined; @@ -267,6 +207,7 @@ export interface PatchRulesOptions extends Partial { rulesClient: RulesClient; rule: SanitizedAlert | null | undefined; } + interface PatchRulesFieldsOptions { anomalyThreshold: AnomalyThresholdOrUndefined; author: AuthorOrUndefined; @@ -328,7 +269,7 @@ export interface ReadRuleOptions { export interface DeleteRuleOptions { ruleId: Id; rulesClient: RulesClient; - ruleStatusClient: IRuleExecutionLogClient; + ruleExecutionLogClient: IRuleExecutionLogClient; } export interface FindRuleOptions { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts index c4cd5d4211c96..703b0a4f5aec1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -11,7 +11,7 @@ import { getFindResultWithSingleHit } from '../routes/__mocks__/request_response import { updatePrepackagedRules } from './update_prepacked_rules'; import { patchRules } from './patch_rules'; import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock'; -import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; + jest.mock('./patch_rules'); describe.each([ @@ -19,12 +19,10 @@ describe.each([ ['RAC', true], ])('updatePrepackagedRules - %s', (_, isRuleRegistryEnabled) => { let rulesClient: ReturnType; - let ruleStatusClient: ReturnType; let savedObjectsClient: ReturnType; beforeEach(() => { rulesClient = rulesClientMock.create(); - ruleStatusClient = ruleExecutionLogClientMock.create(); savedObjectsClient = savedObjectsClientMock.create(); }); @@ -45,7 +43,6 @@ describe.each([ rulesClient, savedObjectsClient, 'default', - ruleStatusClient, [{ ...prepackagedRule, actions }], outputIndex, isRuleRegistryEnabled @@ -77,7 +74,6 @@ describe.each([ rulesClient, savedObjectsClient, 'default', - ruleStatusClient, [{ ...prepackagedRule, ...updatedThreatParams }], 'output-index', isRuleRegistryEnabled diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts index 71ca8cf8f1dfa..f6b4508405c5e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -14,7 +14,6 @@ import { patchRules } from './patch_rules'; import { readRules } from './read_rules'; import { PartialFilter } from '../types'; import { RuleParams } from '../schemas/rule_schemas'; -import { IRuleExecutionLogClient } from '../rule_execution_log/types'; import { legacyMigrate } from './utils'; /** @@ -23,7 +22,6 @@ import { legacyMigrate } from './utils'; * avoid being a "noisy neighbor". * @param rulesClient Alerting client * @param spaceId Current user spaceId - * @param ruleStatusClient Rule execution log client * @param rules The rules to apply the update for * @param outputIndex The output index to apply the update to. */ @@ -31,7 +29,6 @@ export const updatePrepackagedRules = async ( rulesClient: RulesClient, savedObjectsClient: SavedObjectsClientContract, spaceId: string, - ruleStatusClient: IRuleExecutionLogClient, rules: AddPrepackagedRulesSchemaDecoded[], outputIndex: string, isRuleRegistryEnabled: boolean @@ -42,7 +39,6 @@ export const updatePrepackagedRules = async ( rulesClient, savedObjectsClient, spaceId, - ruleStatusClient, ruleChunk, outputIndex, isRuleRegistryEnabled @@ -55,7 +51,6 @@ export const updatePrepackagedRules = async ( * Creates promises of the rules and returns them. * @param rulesClient Alerting client * @param spaceId Current user spaceId - * @param ruleStatusClient Rule execution log client * @param rules The rules to apply the update for * @param outputIndex The output index to apply the update to. * @returns Promise of what was updated. @@ -64,7 +59,6 @@ export const createPromises = ( rulesClient: RulesClient, savedObjectsClient: SavedObjectsClientContract, spaceId: string, - ruleStatusClient: IRuleExecutionLogClient, rules: AddPrepackagedRulesSchemaDecoded[], outputIndex: string, isRuleRegistryEnabled: boolean diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts index b98a95ed1aabc..c027f3b8505a9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts @@ -11,14 +11,12 @@ import { getUpdateMachineLearningSchemaMock, getUpdateRulesSchemaMock, } from '../../../../common/detection_engine/schemas/request/rule_schemas.mock'; -import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; import { getAlertMock } from '../routes/__mocks__/request_responses'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; export const getUpdateRulesOptionsMock = (isRuleRegistryEnabled: boolean) => ({ spaceId: 'default', rulesClient: rulesClientMock.create(), - ruleStatusClient: ruleExecutionLogClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), defaultOutputIndex: '.siem-signals-default', existingRule: getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()), @@ -30,7 +28,6 @@ export const getUpdateRulesOptionsMock = (isRuleRegistryEnabled: boolean) => ({ export const getUpdateMlRulesOptionsMock = (isRuleRegistryEnabled: boolean) => ({ spaceId: 'default', rulesClient: rulesClientMock.create(), - ruleStatusClient: ruleExecutionLogClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), defaultOutputIndex: '.siem-signals-default', existingRule: getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts index 4cc73f48202ab..fff8147da8672 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -20,6 +20,7 @@ import { BaseRuleParams, } from './rule_schemas'; import { assertUnreachable } from '../../../../common/utility_types'; +import { RuleExecutionSummary } from '../../../../common/detection_engine/schemas/common'; import { CreateRulesSchema, CreateTypeSpecific, @@ -31,9 +32,7 @@ import { addTags } from '../rules/add_tags'; import { DEFAULT_MAX_SIGNALS, SERVER_APP_ID } from '../../../../common/constants'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { ResolvedSanitizedRule, SanitizedAlert } from '../../../../../alerting/common'; -import { IRuleStatusSOAttributes } from '../rules/types'; import { transformTags } from '../routes/rules/utils'; -import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; import { transformFromAlertThrottle, transformToAlertThrottle, @@ -42,6 +41,7 @@ import { } from '../rules/utils'; // eslint-disable-next-line no-restricted-imports import { LegacyRuleActions } from '../rule_actions/legacy_types'; +import { mergeRuleExecutionSummary } from '../rule_execution_log'; // These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema // to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for @@ -284,10 +284,10 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => { export const internalRuleToAPIResponse = ( rule: SanitizedAlert | ResolvedSanitizedRule, - ruleStatus?: IRuleStatusSOAttributes, + ruleExecutionSummary?: RuleExecutionSummary | null, legacyRuleActions?: LegacyRuleActions | null ): FullResponseSchema => { - const mergedStatus = ruleStatus ? mergeAlertWithSidecarStatus(rule, ruleStatus) : undefined; + const mergedExecutionSummary = mergeRuleExecutionSummary(rule, ruleExecutionSummary ?? null); const isResolvedRule = (obj: unknown): obj is ResolvedSanitizedRule => (obj as ResolvedSanitizedRule).outcome != null; return { @@ -311,34 +311,7 @@ export const internalRuleToAPIResponse = ( // Actions throttle: transformFromAlertThrottle(rule, legacyRuleActions), actions: transformActions(rule.actions, legacyRuleActions), - // Rule status - status: mergedStatus?.status ?? undefined, - status_date: mergedStatus?.statusDate ?? undefined, - last_failure_at: mergedStatus?.lastFailureAt ?? undefined, - last_success_at: mergedStatus?.lastSuccessAt ?? undefined, - last_failure_message: mergedStatus?.lastFailureMessage ?? undefined, - last_success_message: mergedStatus?.lastSuccessMessage ?? undefined, - last_gap: mergedStatus?.gap ?? undefined, - bulk_create_time_durations: mergedStatus?.bulkCreateTimeDurations ?? undefined, - search_after_time_durations: mergedStatus?.searchAfterTimeDurations ?? undefined, + // Execution summary + execution_summary: mergedExecutionSummary ?? undefined, }; }; - -export const mergeAlertWithSidecarStatus = ( - alert: SanitizedAlert, - status: IRuleStatusSOAttributes -): IRuleStatusSOAttributes => { - if ( - new Date(alert.executionStatus.lastExecutionDate) > new Date(status.statusDate) && - alert.executionStatus.status === 'error' - ) { - return { - ...status, - lastFailureMessage: `Reason: ${alert.executionStatus.error?.reason} Message: ${alert.executionStatus.error?.message}`, - lastFailureAt: alert.executionStatus.lastExecutionDate.toISOString(), - statusDate: alert.executionStatus.lastExecutionDate.toISOString(), - status: RuleExecutionStatus.failed, - }; - } - return status; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_all_statuses.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_all_rule_execution_data.sh similarity index 52% rename from x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_all_statuses.sh rename to x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_all_rule_execution_data.sh index de95ae5680521..82cca250f2d9b 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_all_statuses.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_all_rule_execution_data.sh @@ -10,15 +10,31 @@ set -e ./check_env_variables.sh -# Example: ./delete_all_statuses.sh +# Deletes all data related to rule execution: +# - `siem-detection-engine-rule-execution-info` sidecar saved objects from `.kibana` index +# - fully clears `.kibana-event-log` index + +# Example: ./delete_all_rule_execution_data.sh # https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html + curl -s -k \ -H "Content-Type: application/json" \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X POST ${ELASTICSEARCH_URL}/${KIBANA_INDEX}*/_delete_by_query \ --data '{ "query": { - "exists": { "field": "siem-detection-engine-rule-status" } + "exists": { "field": "siem-detection-engine-rule-execution-info" } + } + }' \ + | jq . + +curl -s -k \ + -H "Content-Type: application/json" \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${ELASTICSEARCH_URL}/.kibana-event-log*/_delete_by_query \ + --data '{ + "query": { + "match_all" : {} } }' \ | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/hard_reset.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/hard_reset.sh index ab377457188aa..9a319d7ad4ce0 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/hard_reset.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/hard_reset.sh @@ -10,15 +10,12 @@ set -e ./check_env_variables.sh -# Clean up and remove all actions and alerts from SIEM -# within saved objects +# Clean up and remove all Detection Engine's saved objects and related data ./delete_all_actions.sh ./delete_all_alerts.sh ./delete_all_alert_tasks.sh +./delete_all_rule_execution_data.sh -# delete all the statuses from the signal index -./delete_all_statuses.sh - -# re-create the signal index +# Re-create the signal index ./delete_signal_index.sh ./post_signal_index.sh diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 6558c48a6af5d..5f6ce6966fc02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -18,14 +18,10 @@ import type { } from '../types'; import { SavedObject } from '../../../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks'; -import { IRuleStatusSOAttributes } from '../../rules/types'; -// eslint-disable-next-line no-restricted-imports -import { legacyRuleStatusSavedObjectType } from '../../rules/legacy_rule_status/legacy_rule_status_saved_object_mappings'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; import { RuleParams } from '../../schemas/rule_schemas'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; import { ALERT_REASON, ALERT_RULE_PARAMETERS, @@ -499,10 +495,6 @@ export const sampleSignalHit = (): SignalHit => ({ type: 'query', threat: [], version: 1, - status: RuleExecutionStatus.succeeded, - status_date: '2020-02-22T16:47:50.047Z', - last_success_at: '2020-02-22T16:47:50.047Z', - last_success_message: 'succeeded', output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, @@ -564,10 +556,6 @@ export const sampleThresholdSignalHit = (): SignalHit => ({ type: 'query', threat: [], version: 1, - status: RuleExecutionStatus.succeeded, - status_date: '2020-02-22T16:47:50.047Z', - last_success_at: '2020-02-22T16:47:50.047Z', - last_success_message: 'succeeded', output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, @@ -879,32 +867,6 @@ export const sampleDocSearchResultsWithSortId = ( export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; export const sampleIdGuid = 'e1e08ddc-5e37-49ff-a258-5393aa44435a'; -export const exampleRuleStatus: () => SavedObject = () => ({ - type: legacyRuleStatusSavedObjectType, - id: '042e6d90-7069-11ea-af8b-0f8ae4fa817e', - attributes: { - statusDate: '2020-03-27T22:55:59.517Z', - status: RuleExecutionStatus.succeeded, - lastFailureAt: null, - lastSuccessAt: '2020-03-27T22:55:59.517Z', - lastFailureMessage: null, - lastSuccessMessage: 'succeeded', - gap: null, - bulkCreateTimeDurations: [], - searchAfterTimeDurations: [], - lastLookBackDate: null, - }, - references: [ - { - id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', - type: 'alert', - name: 'alert_0', - }, - ], - updated_at: '2020-03-27T22:55:59.577Z', - version: 'WzgyMiwxXQ==', -}); - export const mockLogger = loggingSystemMock.createLogger(); export const sampleBulkErrorItem = ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts index e69bbd64faada..e06e8a5cdcf76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts @@ -20,7 +20,6 @@ import { } from '../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { SIGNALS_TEMPLATE_VERSION } from '../routes/index/get_signals_template'; -import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; describe('buildSignal', () => { beforeEach(() => { @@ -88,10 +87,6 @@ describe('buildSignal', () => { type: 'query', threat: [], version: 1, - status: RuleExecutionStatus.succeeded, - status_date: '2020-02-22T16:47:50.047Z', - last_success_at: '2020-02-22T16:47:50.047Z', - last_success_message: 'succeeded', output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, @@ -177,10 +172,6 @@ describe('buildSignal', () => { type: 'query', threat: [], version: 1, - status: RuleExecutionStatus.succeeded, - status_date: '2020-02-22T16:47:50.047Z', - last_success_at: '2020-02-22T16:47:50.047Z', - last_success_message: 'succeeded', output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_log_client.ts deleted file mode 100644 index 2a000b7a31968..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_log_client.ts +++ /dev/null @@ -1,78 +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 { SavedObjectsFindResult } from 'kibana/server'; -import { - IRuleExecutionLogClient, - LogStatusChangeArgs, - FindBulkExecutionLogArgs, - FindBulkExecutionLogResponse, - FindExecutionLogArgs, - GetLastFailuresArgs, - GetCurrentStatusArgs, - GetCurrentStatusBulkArgs, - GetCurrentStatusBulkResult, -} from '../../rule_execution_log'; -import { IRuleStatusSOAttributes } from '../../rules/types'; - -interface PreviewRuleExecutionLogClient extends IRuleExecutionLogClient { - clearWarningsAndErrorsStore: () => void; -} - -export const createWarningsAndErrors = () => { - const warningsAndErrorsStore: LogStatusChangeArgs[] = []; - - const previewRuleExecutionLogClient: PreviewRuleExecutionLogClient = { - find( - args: FindExecutionLogArgs - ): Promise>> { - return Promise.resolve([]); - }, - - findBulk(args: FindBulkExecutionLogArgs): Promise { - return Promise.resolve({}); - }, - - getLastFailures(args: GetLastFailuresArgs): Promise { - return Promise.resolve([]); - }, - - getCurrentStatus(args: GetCurrentStatusArgs): Promise { - return Promise.resolve({ - statusDate: new Date().toISOString(), - status: null, - lastFailureAt: null, - lastFailureMessage: null, - lastSuccessAt: null, - lastSuccessMessage: null, - lastLookBackDate: null, - gap: null, - bulkCreateTimeDurations: null, - searchAfterTimeDurations: null, - }); - }, - - getCurrentStatusBulk(args: GetCurrentStatusBulkArgs): Promise { - return Promise.resolve({}); - }, - - deleteCurrentStatus(ruleId: string): Promise { - return Promise.resolve(); - }, - - logStatusChange(args: LogStatusChangeArgs): Promise { - warningsAndErrorsStore.push(args); - return Promise.resolve(); - }, - - clearWarningsAndErrorsStore() { - warningsAndErrorsStore.length = 0; - }, - }; - - return { previewRuleExecutionLogClient, warningsAndErrorsStore }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_logger.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_logger.ts new file mode 100644 index 0000000000000..0cc6baf55ac28 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_logger.ts @@ -0,0 +1,52 @@ +/* + * 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 { + RuleExecutionLoggerFactory, + RuleExecutionContext, + StatusChangeArgs, +} from '../../rule_execution_log'; + +export interface IPreviewRuleExecutionLogger { + factory: RuleExecutionLoggerFactory; + + logged: { + statusChanges: Array; + }; + + clearLogs(): void; +} + +export const createPreviewRuleExecutionLogger = () => { + let logged: IPreviewRuleExecutionLogger['logged'] = { + statusChanges: [], + }; + + const factory: RuleExecutionLoggerFactory = ( + savedObjectsClient, + eventLogService, + logger, + context + ) => { + return { + context, + + logStatusChange(args: StatusChangeArgs): Promise { + logged.statusChanges.push({ ...context, ...args }); + return Promise.resolve(); + }, + }; + }; + + const clearLogs = (): void => { + logged = { + statusChanges: [], + }; + }; + + return { factory, logged, clearLogs }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index b59922fbd7c9c..c620d51a83382 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -14,6 +14,7 @@ import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mo import { listMock } from '../../../../../lists/server/mocks'; import { buildRuleMessageFactory } from './rule_messages'; import { ExceptionListClient } from '../../../../../lists/server'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; @@ -62,7 +63,7 @@ import { sampleAlertDocAADNoSortIdWithTimestamp, } from './__mocks__/es_results'; import { ShardError } from '../../types'; -import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; +import { ruleExecutionLogMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; const buildRuleMessage = buildRuleMessageFactory({ id: 'fake id', @@ -71,8 +72,6 @@ const buildRuleMessage = buildRuleMessageFactory({ name: 'fake name', }); -const ruleStatusClient = ruleExecutionLogClientMock.create(); - describe('utils', () => { const anchor = '2020-01-01T06:06:06.666Z'; const unix = moment(anchor).valueOf(); @@ -664,26 +663,31 @@ describe('utils', () => { }, }, }; + + const ruleExecutionLogger = ruleExecutionLogMock.logger.create(); mockLogger.warn.mockClear(); + const res = await hasTimestampFields({ timestampField, - ruleName: 'myfakerulename', timestampFieldCapsResponse: timestampFieldCapsResponse as TransportResult< // eslint-disable-next-line @typescript-eslint/no-explicit-any Record >, inputIndices: ['myfa*'], - ruleStatusClient, - ruleId: 'ruleId', - ruleType: 'ruleType', - spaceId: 'default', + ruleExecutionLogger, logger: mockLogger, buildRuleMessage, }); + + expect(res).toBeTruthy(); expect(mockLogger.warn).toHaveBeenCalledWith( 'The following indices are missing the timestamp override field "event.ingested": ["myfakeindex-1","myfakeindex-2"] name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); - expect(res).toBeTruthy(); + expect(ruleExecutionLogger.logStatusChange).toHaveBeenCalledWith({ + newStatus: RuleExecutionStatus['partial failure'], + message: + 'The following indices are missing the timestamp override field "event.ingested": ["myfakeindex-1","myfakeindex-2"]', + }); }); test('returns true when missing timestamp field', async () => { @@ -710,26 +714,31 @@ describe('utils', () => { }, }, }; + + const ruleExecutionLogger = ruleExecutionLogMock.logger.create(); mockLogger.warn.mockClear(); + const res = await hasTimestampFields({ timestampField, - ruleName: 'myfakerulename', timestampFieldCapsResponse: timestampFieldCapsResponse as TransportResult< // eslint-disable-next-line @typescript-eslint/no-explicit-any Record >, inputIndices: ['myfa*'], - ruleStatusClient, - ruleId: 'ruleId', - ruleType: 'ruleType', - spaceId: 'default', + ruleExecutionLogger, logger: mockLogger, buildRuleMessage, }); + + expect(res).toBeTruthy(); expect(mockLogger.warn).toHaveBeenCalledWith( 'The following indices are missing the timestamp field "@timestamp": ["myfakeindex-1","myfakeindex-2"] name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); - expect(res).toBeTruthy(); + expect(ruleExecutionLogger.logStatusChange).toHaveBeenCalledWith({ + newStatus: RuleExecutionStatus['partial failure'], + message: + 'The following indices are missing the timestamp field "@timestamp": ["myfakeindex-1","myfakeindex-2"]', + }); }); test('returns true when missing logs-endpoint.alerts-* index and rule name is Endpoint Security', async () => { @@ -741,26 +750,33 @@ describe('utils', () => { fields: {}, }, }; + + const ruleExecutionLogger = ruleExecutionLogMock.logger.create({ + ruleName: 'Endpoint Security', + }); mockLogger.warn.mockClear(); + const res = await hasTimestampFields({ timestampField, - ruleName: 'Endpoint Security', timestampFieldCapsResponse: timestampFieldCapsResponse as TransportResult< // eslint-disable-next-line @typescript-eslint/no-explicit-any Record >, inputIndices: ['logs-endpoint.alerts-*'], - ruleStatusClient, - ruleId: 'ruleId', - ruleType: 'ruleType', - spaceId: 'default', + ruleExecutionLogger, logger: mockLogger, buildRuleMessage, }); + + expect(res).toBeTruthy(); expect(mockLogger.warn).toHaveBeenCalledWith( 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); - expect(res).toBeTruthy(); + expect(ruleExecutionLogger.logStatusChange).toHaveBeenCalledWith({ + newStatus: RuleExecutionStatus['partial failure'], + message: + 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent.', + }); }); test('returns true when missing logs-endpoint.alerts-* index and rule name is NOT Endpoint Security', async () => { @@ -772,26 +788,35 @@ describe('utils', () => { fields: {}, }, }; + + // SUT uses rule execution logger's context to check the rule name + const ruleExecutionLogger = ruleExecutionLogMock.logger.create({ + ruleName: 'NOT Endpoint Security', + }); + mockLogger.warn.mockClear(); + const res = await hasTimestampFields({ timestampField, - ruleName: 'NOT Endpoint Security', timestampFieldCapsResponse: timestampFieldCapsResponse as TransportResult< // eslint-disable-next-line @typescript-eslint/no-explicit-any Record >, inputIndices: ['logs-endpoint.alerts-*'], - ruleStatusClient, - ruleId: 'ruleId', - ruleType: 'ruleType', - spaceId: 'default', + ruleExecutionLogger, logger: mockLogger, buildRuleMessage, }); + + expect(res).toBeTruthy(); expect(mockLogger.warn).toHaveBeenCalledWith( 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); - expect(res).toBeTruthy(); + expect(ruleExecutionLogger.logStatusChange).toHaveBeenCalledWith({ + newStatus: RuleExecutionStatus['partial failure'], + message: + 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated.', + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index de2e897578880..21f7f90a95de9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -21,7 +21,7 @@ import { TimestampOverrideOrUndefined, Privilege, RuleExecutionStatus, -} from '../../../../common/detection_engine/schemas/common/schemas'; +} from '../../../../common/detection_engine/schemas/common'; import { ElasticsearchClient, Logger, @@ -60,7 +60,7 @@ import { } from '../schemas/rule_schemas'; import { RACAlert, WrappedRACAlert } from '../rule_types/types'; import { SearchTypes } from '../../../../common/detection_engine/types'; -import { IRuleExecutionLogClient } from '../rule_execution_log/types'; +import { IRuleExecutionLogger } from '../rule_execution_log'; import { withSecuritySpan } from '../../../utils/with_security_span'; interface SortExceptionsReturn { @@ -89,22 +89,9 @@ export const hasReadIndexPrivileges = async (args: { privileges: Privilege; logger: Logger; buildRuleMessage: BuildRuleMessage; - ruleStatusClient: IRuleExecutionLogClient; - ruleId: string; - ruleName: string; - ruleType: string; - spaceId: string; + ruleExecutionLogger: IRuleExecutionLogger; }): Promise => { - const { - privileges, - logger, - buildRuleMessage, - ruleStatusClient, - ruleId, - ruleName, - ruleType, - spaceId, - } = args; + const { privileges, logger, buildRuleMessage, ruleExecutionLogger } = args; const indexNames = Object.keys(privileges.index); const [, indexesWithNoReadPrivileges] = partition( @@ -119,13 +106,9 @@ export const hasReadIndexPrivileges = async (args: { indexesWithNoReadPrivileges )}`; logger.warn(buildRuleMessage(errorString)); - await ruleStatusClient.logStatusChange({ - message: errorString, - ruleId, - ruleName, - ruleType, - spaceId, + await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus['partial failure'], + message: errorString, }); return true; } @@ -134,32 +117,26 @@ export const hasReadIndexPrivileges = async (args: { export const hasTimestampFields = async (args: { timestampField: string; - ruleName: string; // any is derived from here // node_modules/@elastic/elasticsearch/lib/api/kibana.d.ts // eslint-disable-next-line @typescript-eslint/no-explicit-any timestampFieldCapsResponse: TransportResult, unknown>; inputIndices: string[]; - ruleStatusClient: IRuleExecutionLogClient; - ruleId: string; - spaceId: string; - ruleType: string; + ruleExecutionLogger: IRuleExecutionLogger; logger: Logger; buildRuleMessage: BuildRuleMessage; }): Promise => { const { timestampField, - ruleName, timestampFieldCapsResponse, inputIndices, - ruleStatusClient, - ruleId, - ruleType, - spaceId, + ruleExecutionLogger, logger, buildRuleMessage, } = args; + const { ruleName } = ruleExecutionLogger.context; + if (isEmpty(timestampFieldCapsResponse.body.indices)) { const errorString = `This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ${JSON.stringify( inputIndices @@ -169,13 +146,9 @@ export const hasTimestampFields = async (args: { : '' }`; logger.warn(buildRuleMessage(errorString.trimEnd())); - await ruleStatusClient.logStatusChange({ - message: errorString.trimEnd(), - ruleId, - ruleName, - ruleType, - spaceId, + await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus['partial failure'], + message: errorString.trimEnd(), }); return true; } else if ( @@ -195,15 +168,13 @@ export const hasTimestampFields = async (args: { ? timestampFieldCapsResponse.body.indices : timestampFieldCapsResponse.body.fields[timestampField]?.unmapped?.indices )}`; + logger.warn(buildRuleMessage(errorString)); - await ruleStatusClient.logStatusChange({ - message: errorString, - ruleId, - ruleName, - ruleType, - spaceId, + await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus['partial failure'], + message: errorString, }); + return true; } return false; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 91118d0ef4e89..784e2ed759798 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -67,7 +67,10 @@ import { PolicyWatcher } from './endpoint/lib/policy/license_watch'; import { migrateArtifactsToFleet } from './endpoint/lib/artifacts/migrate_artifacts_to_fleet'; import aadFieldConversion from './lib/detection_engine/routes/index/signal_aad_mapping.json'; import previewPolicy from './lib/detection_engine/routes/index/preview_policy.json'; -import { registerEventLogProvider } from './lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider'; +import { + registerEventLogProvider, + ruleExecutionLoggerFactory, +} from './lib/detection_engine/rule_execution_log'; import { getKibanaPrivilegesFeaturePrivileges, getCasesKibanaFeature } from './features'; import { EndpointMetadataService } from './endpoint/services/metadata'; import { CreateRuleOptions } from './lib/detection_engine/rule_types/types'; @@ -228,6 +231,7 @@ export class Plugin implements ISecuritySolutionPlugin { config: this.config, ruleDataClient, eventLogService, + ruleExecutionLoggerFactory, }; const securityRuleTypeWrapper = createSecurityRuleTypeWrapper(securityRuleTypeOptions); diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index 26bb56113e635..c43b667896c11 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { memoize } from 'lodash'; + import { Logger, KibanaRequest, RequestHandlerContext } from 'kibana/server'; import { DEFAULT_SPACE_ID } from '../common/constants'; import { AppClientFactory } from './client'; import { ConfigType } from './config'; -import { RuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/rule_execution_log_client'; +import { ruleExecutionLogClientFactory } from './lib/detection_engine/rule_execution_log'; import { buildFrameworkRequest } from './lib/timeline/utils/common'; import { SecuritySolutionPluginCoreSetupDependencies, @@ -101,14 +103,13 @@ export class RequestContextFactory implements IRequestContextFactory { getRuleDataService: () => ruleRegistry.ruleDataService, - getExecutionLogClient: () => - new RuleExecutionLogClient({ - underlyingClient: config.ruleExecutionLog.underlyingClient, - savedObjectsClient: context.core.savedObjects.client, - eventLogService: plugins.eventLog, - eventLogClient: startPlugins.eventLog.getClient(request), - logger, - }), + getExecutionLogClient: memoize(() => + ruleExecutionLogClientFactory( + context.core.savedObjects.client, + startPlugins.eventLog.getClient(request), + logger + ) + ), getExceptionListClient: () => { if (!lists) { diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index d7cc745847f31..13788cbae9fde 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -36,7 +36,7 @@ import { deleteRulesBulkRoute } from '../lib/detection_engine/routes/rules/delet import { performBulkActionRoute } from '../lib/detection_engine/routes/rules/perform_bulk_action_route'; import { importRulesRoute } from '../lib/detection_engine/routes/rules/import_rules_route'; import { exportRulesRoute } from '../lib/detection_engine/routes/rules/export_rules_route'; -import { findRuleStatusInternalRoute } from '../lib/detection_engine/routes/rules/find_rule_status_internal_route'; +import { getRuleExecutionEventsRoute } from '../lib/detection_engine/routes/rules/get_rule_execution_events_route'; import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; import { createTimelinesRoute, @@ -86,7 +86,7 @@ export const initRoutes = ( ) => { const isRuleRegistryEnabled = ruleDataClient != null; // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules - // All REST rule creation, deletion, updating, etc...... + // All REST rule creation, deletion, updating, etc createRulesRoute(router, ml, isRuleRegistryEnabled); readRulesRoute(router, logger, isRuleRegistryEnabled); updateRulesRoute(router, ml, isRuleRegistryEnabled); @@ -114,6 +114,8 @@ export const initRoutes = ( deleteRulesBulkRoute(router, isRuleRegistryEnabled); performBulkActionRoute(router, ml, logger, isRuleRegistryEnabled); + getRuleExecutionEventsRoute(router); + createTimelinesRoute(router, config, security); patchTimelinesRoute(router, config, security); importRulesRoute(router, config, ml, isRuleRegistryEnabled); @@ -134,8 +136,6 @@ export const initRoutes = ( persistNoteRoute(router, config, security); persistPinnedEventRoute(router, config, security); - findRuleStatusInternalRoute(router); - // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals // POST /api/detection_engine/signals/status // Example usage can be found in security_solution/server/lib/detection_engine/scripts/signals diff --git a/x-pack/plugins/security_solution/server/saved_objects.ts b/x-pack/plugins/security_solution/server/saved_objects.ts index 53618d738984b..0eca269a018ce 100644 --- a/x-pack/plugins/security_solution/server/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/saved_objects.ts @@ -9,10 +9,9 @@ import { CoreSetup } from '../../../../src/core/server'; import { noteType, pinnedEventType, timelineType } from './lib/timeline/saved_object_mappings'; // eslint-disable-next-line no-restricted-imports -import { legacyRuleStatusType } from './lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings'; -import { ruleAssetType } from './lib/detection_engine/rules/rule_asset/rule_asset_saved_object_mappings'; -// eslint-disable-next-line no-restricted-imports import { legacyType as legacyRuleActionsType } from './lib/detection_engine/rule_actions/legacy_saved_object_mappings'; +import { ruleExecutionInfoType } from './lib/detection_engine/rule_execution_log'; +import { ruleAssetType } from './lib/detection_engine/rules/rule_asset/rule_asset_saved_object_mappings'; import { type as signalsMigrationType } from './lib/detection_engine/migrations/saved_objects'; import { exceptionsArtifactType, @@ -23,7 +22,7 @@ const types = [ noteType, pinnedEventType, legacyRuleActionsType, - legacyRuleStatusType, + ruleExecutionInfoType, ruleAssetType, timelineType, exceptionsArtifactType, diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index 75686d7834070..3be7a5e998b76 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -15,7 +15,7 @@ import type { IRuleDataService } from '../../rule_registry/server'; import { AppClient } from './client'; import { ConfigType } from './config'; -import { IRuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/types'; +import { IRuleExecutionLogClient } from './lib/detection_engine/rule_execution_log'; import { FrameworkRequest } from './lib/framework'; import { EndpointAuthz } from '../common/endpoint/types/authz'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e17ed6f278acd..4c4910cede8e7 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22148,13 +22148,11 @@ "xpack.securitySolution.detectionEngine.ruleDetails.activateRuleLabel": "有効化", "xpack.securitySolution.detectionEngine.ruleDetails.backToRulesButton": "ルール", "xpack.securitySolution.detectionEngine.ruleDetails.deletedRule": "削除されたルール", - "xpack.securitySolution.detectionEngine.ruleDetails.errorCalloutTitle": "ルール失敗", "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "例外", "xpack.securitySolution.detectionEngine.ruleDetails.experimentalDescription": "実験的", "xpack.securitySolution.detectionEngine.ruleDetails.failureHistoryTab": "エラー履歴", "xpack.securitySolution.detectionEngine.ruleDetails.lastFiveErrorsTitle": "最後の5件のエラー", "xpack.securitySolution.detectionEngine.ruleDetails.pageTitle": "ルール詳細", - "xpack.securitySolution.detectionEngine.ruleDetails.partialErrorCalloutTitle": "警告", "xpack.securitySolution.detectionEngine.ruleDetails.ruleCreationDescription": "作成者:{by} 日付:{date}", "xpack.securitySolution.detectionEngine.ruleDetails.ruleUpdateDescription": "更新者:{by} 日付:{date}", "xpack.securitySolution.detectionEngine.ruleDetails.statusFailedAtColumn": "失敗", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7850bfaba35c2..8e4a9aaf86138 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -22484,13 +22484,11 @@ "xpack.securitySolution.detectionEngine.ruleDetails.activateRuleLabel": "激活", "xpack.securitySolution.detectionEngine.ruleDetails.backToRulesButton": "规则", "xpack.securitySolution.detectionEngine.ruleDetails.deletedRule": "已删除规则", - "xpack.securitySolution.detectionEngine.ruleDetails.errorCalloutTitle": "规则错误位置", "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "例外", "xpack.securitySolution.detectionEngine.ruleDetails.experimentalDescription": "实验性", "xpack.securitySolution.detectionEngine.ruleDetails.failureHistoryTab": "失败历史记录", "xpack.securitySolution.detectionEngine.ruleDetails.lastFiveErrorsTitle": "上五个错误", "xpack.securitySolution.detectionEngine.ruleDetails.pageTitle": "规则详情", - "xpack.securitySolution.detectionEngine.ruleDetails.partialErrorCalloutTitle": "警告于", "xpack.securitySolution.detectionEngine.ruleDetails.ruleCreationDescription": "由 {by} 于 {date}创建", "xpack.securitySolution.detectionEngine.ruleDetails.ruleUpdateDescription": "由 {by} 于 {date}更新", "xpack.securitySolution.detectionEngine.ruleDetails.statusFailedAtColumn": "失败于", diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts index 6d2610dfce186..972fdf8f7c72e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts @@ -70,7 +70,9 @@ export default ({ getService }: FtrProviderContext) => { .set('kbn-xsrf', 'true') .query({ id }) .expect(200); - expect(body?.last_success_message).to.eql( + + // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe + expect(body?.execution_summary?.last_execution.message).to.eql( `This rule may not have the required read privileges to the following indices/index patterns: ["${index[0]}"]` ); @@ -96,7 +98,9 @@ export default ({ getService }: FtrProviderContext) => { .set('kbn-xsrf', 'true') .query({ id }) .expect(200); - expect(body?.last_success_message).to.eql( + + // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe + expect(body?.execution_summary?.last_execution.message).to.eql( `This rule may not have the required read privileges to the following indices/index patterns: ["${index[0]}"]` ); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index 4a572f94b959d..29237d065fd3a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -120,8 +120,9 @@ export default ({ getService }: FtrProviderContext) => { .query({ id: body.id }) .expect(200); - expect(rule.status).to.eql('partial failure'); - expect(rule.last_success_message).to.eql( + // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe + expect(rule?.execution_summary?.last_execution.status).to.eql('partial failure'); + expect(rule?.execution_summary?.last_execution.message).to.eql( 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["does-not-exist-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated.' ); }); @@ -307,8 +308,9 @@ export default ({ getService }: FtrProviderContext) => { .query({ id: bodyId }) .expect(200); - expect(rule?.status).to.eql('partial failure'); - expect(rule?.last_success_message).to.eql( + // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe + expect(rule?.execution_summary?.last_execution.status).to.eql('partial failure'); + expect(rule?.execution_summary?.last_execution.message).to.eql( 'The following indices are missing the timestamp override field "event.ingested": ["myfakeindex-1"]' ); }); @@ -335,7 +337,8 @@ export default ({ getService }: FtrProviderContext) => { .query({ id: bodyId }) .expect(200); - expect(rule.status).to.eql('partial failure'); + // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe + expect(rule?.execution_summary?.last_execution.status).to.eql('partial failure'); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index a50a9bf5bbefe..b85b1fcd4003e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -114,7 +114,9 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare = removeServerGeneratedProperties(ruleResponse); expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock(true)); - expect(rule.status).to.eql('succeeded'); + + // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe + expect(rule?.execution_summary?.last_execution.status).to.eql('succeeded'); }); }); @@ -477,7 +479,9 @@ export default ({ getService }: FtrProviderContext) => { .set('kbn-xsrf', 'true') .query({ id }) .expect(200); - expect(body.last_failure_message).to.contain( + + // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe + expect(body?.execution_summary?.last_execution.message).to.contain( 'execution has exceeded its allotted interval' ); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 8d6764b8a6405..077fba52666df 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -1191,7 +1191,10 @@ export default ({ getService }: FtrProviderContext) => { .get(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') .query({ id: ruleResponse.id }); - const initialStatusDate = new Date(statusResponse.body.status_date); + + // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe + const ruleStatusDate = statusResponse.body?.execution_summary?.last_execution.date; + const initialStatusDate = new Date(ruleStatusDate); const initialSignal = signals.hits.hits[0]; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts index 66d33de2ba4da..04586999aa163 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { IRuleStatusSOAttributes } from '../../../../plugins/security_solution/server/lib/detection_engine/rules/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -97,94 +96,6 @@ export default ({ getService }: FtrProviderContext): void => { '7d' ); }); - - it('migrates legacy siem-detection-engine-rule-status to use saved object references', async () => { - const response = await es.get<{ - 'siem-detection-engine-rule-status': { - alertId: string; - }; - references: [{}]; - }>( - { - index: '.kibana', - id: 'siem-detection-engine-rule-status:d62d2980-27c4-11ec-92b0-f7b47106bb35', - }, - { meta: true } - ); - expect(response.statusCode).to.eql(200); - - // references exist and are expected values - expect(response.body._source?.references).to.eql([ - { - name: 'alert_0', - id: 'fb1046a0-0452-11ec-9b15-d13d79d162f3', - type: 'alert', - }, - ]); - - // alertId no longer exist - expect(response.body._source?.['siem-detection-engine-rule-status'].alertId).to.eql( - undefined - ); - }); - - it('migrates legacy siem-detection-engine-rule-status and retains other attributes as the same attributes as before', async () => { - const response = await es.get<{ - 'siem-detection-engine-rule-status': IRuleStatusSOAttributes; - }>( - { - index: '.kibana', - id: 'siem-detection-engine-rule-status:d62d2980-27c4-11ec-92b0-f7b47106bb35', - }, - { meta: true } - ); - expect(response.statusCode).to.eql(200); - - expect(response.body._source?.['siem-detection-engine-rule-status']).to.eql({ - statusDate: '2021-10-11T20:51:26.622Z', - status: 'succeeded', - lastFailureAt: '2021-10-11T18:10:08.982Z', - lastSuccessAt: '2021-10-11T20:51:26.622Z', - lastFailureMessage: - '4 days (323690920ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances. name: "Threshy" id: "fb1046a0-0452-11ec-9b15-d13d79d162f3" rule id: "b789c80f-f6d8-41f1-8b4f-b4a23342cde2" signals index: ".siem-signals-spong-default"', - lastSuccessMessage: 'succeeded', - gap: '4 days', - bulkCreateTimeDurations: ['34.49'], - searchAfterTimeDurations: ['62.58'], - lastLookBackDate: null, - }); - }); - - // If alertId is not a string/null ensure it is removed as the mapping was removed and so the migration would fail. - // Specific test for this as e2e tests will not catch the mismatch and we can't use the migration test harness - // since this SO is not importable/exportable via the SOM. - // For details see: https://github.com/elastic/kibana/issues/116423 - it('migrates legacy siem-detection-engine-rule-status and removes alertId when not a string', async () => { - const response = await es.get<{ - 'siem-detection-engine-rule-status': IRuleStatusSOAttributes; - }>( - { - index: '.kibana', - id: 'siem-detection-engine-rule-status:d62d2980-27c4-11ec-92b0-f7b47106bb36', - }, - { meta: true } - ); - expect(response.statusCode).to.eql(200); - - expect(response.body._source?.['siem-detection-engine-rule-status']).to.eql({ - statusDate: '2021-10-11T20:51:26.622Z', - status: 'succeeded', - lastFailureAt: '2021-10-11T18:10:08.982Z', - lastSuccessAt: '2021-10-11T20:51:26.622Z', - lastFailureMessage: - '4 days (323690920ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances. name: "Threshy" id: "fb1046a0-0452-11ec-9b15-d13d79d162f3" rule id: "b789c80f-f6d8-41f1-8b4f-b4a23342cde2" signals index: ".siem-signals-spong-default"', - lastSuccessMessage: 'succeeded', - gap: '4 days', - bulkCreateTimeDurations: ['34.49'], - searchAfterTimeDurations: ['62.58'], - lastLookBackDate: null, - }); - }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 2f9fba7430d59..04bf0eec6f5b8 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -66,18 +66,10 @@ export const removeServerGeneratedProperties = ( ): Partial => { const { /* eslint-disable @typescript-eslint/naming-convention */ + id, created_at, updated_at, - id, - last_failure_at, - last_failure_message, - last_success_at, - last_success_message, - last_gap, - search_after_time_durations, - bulk_create_time_durations, - status, - status_date, + execution_summary, /* eslint-enable @typescript-eslint/naming-convention */ ...removedProperties } = rule; @@ -538,18 +530,18 @@ export const deleteAllTimelines = async (es: Client): Promise => { }; /** - * Remove all rules statuses from the .kibana index + * Remove all rules execution info saved objects from the .kibana index * This will retry 20 times before giving up and hopefully still not interfere with other tests * @param es The ElasticSearch handle * @param log The tooling logger */ -export const deleteAllRulesStatuses = async (es: Client, log: ToolingLog): Promise => { +export const deleteAllRuleExecutionInfo = async (es: Client, log: ToolingLog): Promise => { return countDownES( async () => { return es.deleteByQuery( { index: '.kibana', - q: 'type:siem-detection-engine-rule-status', + q: 'type:siem-detection-engine-rule-execution-info', wait_for_completion: true, refresh: true, body: {}, @@ -557,7 +549,7 @@ export const deleteAllRulesStatuses = async (es: Client, log: ToolingLog): Promi { meta: true } ); }, - 'deleteAllRulesStatuses', + 'deleteAllRuleExecutionInfo', log ); }; @@ -1369,9 +1361,13 @@ export const waitForRuleSuccessOrStatus = async ( )}, status: ${JSON.stringify(response.status)}` ); } + + // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe const rule = response.body; + const ruleStatus = rule?.execution_summary?.last_execution.status; + const ruleStatusDate = rule?.execution_summary?.last_execution.date; - if (rule?.status !== status) { + if (ruleStatus !== status) { log.debug( `Did not get an expected status of ${status} while waiting for a rule success or status for rule id ${id} (waitForRuleSuccessOrStatus). Will continue retrying until status is found. body: ${JSON.stringify( response.body @@ -1380,8 +1376,8 @@ export const waitForRuleSuccessOrStatus = async ( } return ( rule != null && - rule.status === status && - (afterDate ? new Date(rule.status_date) > afterDate : true) + ruleStatus === status && + (afterDate ? new Date(ruleStatusDate) > afterDate : true) ); } catch (e) { if ((e as Error).message.includes('got 503 "Service Unavailable"')) { diff --git a/x-pack/test/functional/es_archives/security_solution/migrations/data.json b/x-pack/test/functional/es_archives/security_solution/migrations/data.json index 89eb207b392e8..832c56c38e497 100644 --- a/x-pack/test/functional/es_archives/security_solution/migrations/data.json +++ b/x-pack/test/functional/es_archives/security_solution/migrations/data.json @@ -29,66 +29,3 @@ } } } - -{ - "type": "doc", - "value": { - "id": "siem-detection-engine-rule-status:d62d2980-27c4-11ec-92b0-f7b47106bb35", - "index": ".kibana_1", - "source": { - "siem-detection-engine-rule-status": { - "alertId": "fb1046a0-0452-11ec-9b15-d13d79d162f3", - "statusDate": "2021-10-11T20:51:26.622Z", - "status": "succeeded", - "lastFailureAt": "2021-10-11T18:10:08.982Z", - "lastSuccessAt": "2021-10-11T20:51:26.622Z", - "lastFailureMessage": "4 days (323690920ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances. name: \"Threshy\" id: \"fb1046a0-0452-11ec-9b15-d13d79d162f3\" rule id: \"b789c80f-f6d8-41f1-8b4f-b4a23342cde2\" signals index: \".siem-signals-spong-default\"", - "lastSuccessMessage": "succeeded", - "gap": "4 days", - "bulkCreateTimeDurations": [ - "34.49" - ], - "searchAfterTimeDurations": [ - "62.58" - ], - "lastLookBackDate": null - }, - "type": "siem-detection-engine-rule-status", - "references": [], - "coreMigrationVersion": "7.14.0", - "updated_at": "2021-10-11T20:51:26.657Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "siem-detection-engine-rule-status:d62d2980-27c4-11ec-92b0-f7b47106bb36", - "index": ".kibana_1", - "source": { - "siem-detection-engine-rule-status": { - "alertId": 1337, - "statusDate": "2021-10-11T20:51:26.622Z", - "status": "succeeded", - "lastFailureAt": "2021-10-11T18:10:08.982Z", - "lastSuccessAt": "2021-10-11T20:51:26.622Z", - "lastFailureMessage": "4 days (323690920ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances. name: \"Threshy\" id: \"fb1046a0-0452-11ec-9b15-d13d79d162f3\" rule id: \"b789c80f-f6d8-41f1-8b4f-b4a23342cde2\" signals index: \".siem-signals-spong-default\"", - "lastSuccessMessage": "succeeded", - "gap": "4 days", - "bulkCreateTimeDurations": [ - "34.49" - ], - "searchAfterTimeDurations": [ - "62.58" - ], - "lastLookBackDate": null - }, - "type": "siem-detection-engine-rule-status", - "references": [], - "coreMigrationVersion": "7.14.0", - "updated_at": "2021-10-11T20:51:26.657Z" - } - } -} - diff --git a/x-pack/test/functional/es_archives/security_solution/migrations/mappings.json b/x-pack/test/functional/es_archives/security_solution/migrations/mappings.json index 8728ec4ad74a1..1e2b89a3052c8 100644 --- a/x-pack/test/functional/es_archives/security_solution/migrations/mappings.json +++ b/x-pack/test/functional/es_archives/security_solution/migrations/mappings.json @@ -72,7 +72,6 @@ "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", "alert": "4373936518133d7f118940e0441bbf40", - "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", "ingest-package-policies": "4525de5ba9d036d8322ecfba3bca93f8", "map": "9134b47593116d7953f6adba096fc463", "legacy-url-alias": "e8dd3b6056ad7e1de32523f457310ecb", @@ -2339,43 +2338,6 @@ } } }, - "siem-detection-engine-rule-status": { - "properties": { - "alertId": { - "type": "keyword" - }, - "bulkCreateTimeDurations": { - "type": "float" - }, - "gap": { - "type": "text" - }, - "lastFailureAt": { - "type": "date" - }, - "lastFailureMessage": { - "type": "text" - }, - "lastLookBackDate": { - "type": "date" - }, - "lastSuccessAt": { - "type": "date" - }, - "lastSuccessMessage": { - "type": "text" - }, - "searchAfterTimeDurations": { - "type": "float" - }, - "status": { - "type": "keyword" - }, - "statusDate": { - "type": "date" - } - } - }, "siem-ui-timeline": { "properties": { "columns": { diff --git a/yarn.lock b/yarn.lock index 369c5edf179d2..47a01e6cb8aee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23546,10 +23546,10 @@ react-popper@^2.2.4: react-fast-compare "^3.0.1" warning "^4.0.2" -react-query@^3.28.0: - version "3.28.0" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.28.0.tgz#1bfe12944860b2b773680054de37f19438f59d1d" - integrity sha512-OeX+nRqs7Zi0MvvtaKxKWE4N966UGtqSVuedOsz8cJh9eW195fgtYZ9nW3hZjIPPmeDY1PkArLUiV4wZvNRDPw== +react-query@^3.34.0: + version "3.34.8" + resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.34.8.tgz#a3be8523fd95f766b04c32847a1b58d8231db03c" + integrity sha512-pl9e2VmVbgKf29Qn/WpmFVtB2g17JPqLLyOQg3GfSs/S2WABvip5xlT464vfXtilLPcJVg9bEHHlqmC38/nvDw== dependencies: "@babel/runtime" "^7.5.5" broadcast-channel "^3.4.1" From 1f0906ea1baaca956cc850a10539cbc28ba14d90 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 20 Jan 2022 14:20:50 -0700 Subject: [PATCH 23/43] Fixes the producer to be SERVER_APP_ID (#123504) ## Summary See: https://github.com/elastic/kibana/issues/123500 This is a one line fix to change the `producer` to be the `SERVER_APP_ID` like the others Before: Screen Shot 2022-01-20 at 11 51 17 AM After: Screen Shot 2022-01-20 at 12 39 05 PM --- .../rule_types/saved_query/create_saved_query_alert_type.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/saved_query/create_saved_query_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/saved_query/create_saved_query_alert_type.ts index 2ed66ba652644..2722f31f3eacd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/saved_query/create_saved_query_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/saved_query/create_saved_query_alert_type.ts @@ -7,6 +7,7 @@ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; import { SAVED_QUERY_RULE_TYPE_ID } from '@kbn/securitysolution-rules'; +import { SERVER_APP_ID } from '../../../../../common/constants'; import { CompleteRule, @@ -50,7 +51,7 @@ export const createSavedQueryAlertType = ( }, minimumLicenseRequired: 'basic', isExportable: false, - producer: 'security-solution', + producer: SERVER_APP_ID, async executor(execOptions) { const { runOpts: { From d2db83d017dfba21db5cd70ecbfb7488642926b2 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 20 Jan 2022 16:25:00 -0500 Subject: [PATCH 24/43] Revert "skip flaky suite (#123495)" This reverts commit 35f721db9bef5ffd0d2787a9da83a568c9c598ca. --- .../endpoint/validators/base_validator.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts index 9d19ed4178a2a..728e3b8559ed3 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts @@ -21,8 +21,7 @@ import { GLOBAL_ARTIFACT_TAG, } from '../../../../common/endpoint/service/artifacts'; -// FLAKY: https://github.com/elastic/kibana/issues/123495 -describe.skip('When using Artifacts Exceptions BaseValidator', () => { +describe('When using Artifacts Exceptions BaseValidator', () => { let endpointAppContextServices: EndpointAppContextService; let kibanaRequest: ReturnType; let exceptionLikeItem: ExceptionItemLikeOptions; From f5eec419dfd4d1f5cca9c83d2bc74a973c17ad90 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 20 Jan 2022 15:37:30 -0600 Subject: [PATCH 25/43] [Alerting] Add dropdown for number of executions in Rule Details view (#122595) * [Alerting] Add dropdown for number of executions in Rule Details view * Fix tests * Fix missing prop in tests * Add unit test for numberOfExecutions api param * Fix i18n and improve effect use * Fix typescript nit * Fix inline snapshot * Fix test snapshots * Fix snapshot * Fix snapshot * Switch api to number of executions instead of implied date * Fix refresh button and event log filter query * Fix tests for non-numberOfExecutions api calls * Remove extraneous import * Switch execution results to separate query * Fix test * Fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../lib/alert_summary_from_event_log.test.ts | 30 ++- .../lib/alert_summary_from_event_log.ts | 29 ++- .../routes/get_rule_alert_summary.test.ts | 1 + .../server/routes/get_rule_alert_summary.ts | 3 + .../server/rules_client/rules_client.ts | 52 +++-- .../tests/get_alert_summary.test.ts | 35 ++- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../lib/alert_api/alert_summary.test.ts | 5 + .../lib/alert_api/alert_summary.ts | 7 +- .../components/alert_details.test.tsx | 1 + .../components/alert_details.tsx | 3 + .../components/alert_details_route.tsx | 1 + .../alert_details/components/alerts.test.tsx | 2 + .../alert_details/components/alerts.tsx | 13 +- .../components/alerts_route.test.tsx | 2 +- .../alert_details/components/alerts_route.tsx | 48 ++++- .../execution_duration_chart.test.tsx | 16 +- .../components/execution_duration_chart.tsx | 200 ++++++++++-------- .../with_bulk_alert_api_operations.tsx | 6 +- 20 files changed, 327 insertions(+), 129 deletions(-) diff --git a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts index 1b1b345b94ef6..6290d5d213046 100644 --- a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts +++ b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts @@ -19,9 +19,11 @@ describe('alertSummaryFromEventLog', () => { test('no events and muted ids', async () => { const rule = createRule({}); const events: IValidatedEvent[] = []; + const executionEvents: IValidatedEvent[] = []; const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart, dateEnd, }); @@ -63,9 +65,11 @@ describe('alertSummaryFromEventLog', () => { muteAll: true, }); const events: IValidatedEvent[] = []; + const executionEvents: IValidatedEvent[] = []; const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart: dateString(dateEnd, ONE_HOUR_IN_MILLIS), dateEnd: dateString(dateEnd, ONE_HOUR_IN_MILLIS * 2), }); @@ -102,9 +106,11 @@ describe('alertSummaryFromEventLog', () => { mutedInstanceIds: ['alert-1', 'alert-2'], }); const events: IValidatedEvent[] = []; + const executionEvents: IValidatedEvent[] = []; const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart, dateEnd, }); @@ -138,10 +144,12 @@ describe('alertSummaryFromEventLog', () => { const rule = createRule({}); const eventsFactory = new EventsFactory(); const events = eventsFactory.addExecute().advanceTime(10000).addExecute().getEvents(); + const executionEvents = eventsFactory.getEvents(); const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart, dateEnd, }); @@ -166,10 +174,12 @@ describe('alertSummaryFromEventLog', () => { .advanceTime(10000) .addExecute('rut roh!') .getEvents(); + const executionEvents = eventsFactory.getEvents(); const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart, dateEnd, }); @@ -207,10 +217,12 @@ describe('alertSummaryFromEventLog', () => { .addExecute() .addRecoveredAlert('alert-1') .getEvents(); + const executionEvents = eventsFactory.getEvents(); const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart, dateEnd, }); @@ -246,10 +258,12 @@ describe('alertSummaryFromEventLog', () => { .addExecute() .addLegacyResolvedAlert('alert-1') .getEvents(); + const executionEvents = eventsFactory.getEvents(); const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart, dateEnd, }); @@ -284,10 +298,12 @@ describe('alertSummaryFromEventLog', () => { .addExecute() .addRecoveredAlert('alert-1') .getEvents(); + const executionEvents = eventsFactory.getEvents(); const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart, dateEnd, }); @@ -323,10 +339,12 @@ describe('alertSummaryFromEventLog', () => { .addExecute() .addActiveAlert('alert-1', 'action group A') .getEvents(); + const executionEvents = eventsFactory.getEvents(); const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart, dateEnd, }); @@ -362,10 +380,12 @@ describe('alertSummaryFromEventLog', () => { .addExecute() .addActiveAlert('alert-1', undefined) .getEvents(); + const executionEvents = eventsFactory.getEvents(); const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart, dateEnd, }); @@ -401,10 +421,12 @@ describe('alertSummaryFromEventLog', () => { .addExecute() .addActiveAlert('alert-1', 'action group B') .getEvents(); + const executionEvents = eventsFactory.getEvents(); const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart, dateEnd, }); @@ -439,14 +461,15 @@ describe('alertSummaryFromEventLog', () => { .addExecute() .addActiveAlert('alert-1', 'action group A') .getEvents(); + const executionEvents = eventsFactory.getEvents(); const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart, dateEnd, }); - const { lastRun, status, alerts, executionDuration } = summary; expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` Object { @@ -481,14 +504,15 @@ describe('alertSummaryFromEventLog', () => { .addActiveAlert('alert-1', 'action group A') .addRecoveredAlert('alert-2') .getEvents(); + const executionEvents = eventsFactory.getEvents(); const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart, dateEnd, }); - const { lastRun, status, alerts, executionDuration } = summary; expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` Object { @@ -536,10 +560,12 @@ describe('alertSummaryFromEventLog', () => { .addExecute() .addActiveAlert('alert-1', 'action group B') .getEvents(); + const executionEvents = eventsFactory.getEvents(); const summary: AlertSummary = alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart, dateEnd, }); diff --git a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts index 808c3c38e3f7e..9a40c4ebf1940 100644 --- a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts +++ b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts @@ -15,13 +15,14 @@ const Millis2Nanos = 1000 * 1000; export interface AlertSummaryFromEventLogParams { rule: SanitizedAlert<{ bar: boolean }>; events: IEvent[]; + executionEvents: IEvent[]; dateStart: string; dateEnd: string; } export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams): AlertSummary { // initialize the result - const { rule, events, dateStart, dateEnd } = params; + const { rule, events, executionEvents, dateStart, dateEnd } = params; const alertSummary: AlertSummary = { id: rule.id, name: rule.name, @@ -57,6 +58,7 @@ export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams) if (provider !== EVENT_LOG_PROVIDER) continue; const action = event?.event?.action; + if (action === undefined) continue; if (action === EVENT_LOG_ACTIONS.execute) { @@ -73,14 +75,6 @@ export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams) alertSummary.status = 'OK'; } - if (event?.event?.duration) { - const eventDirationMillis = event?.event?.duration / Millis2Nanos; - eventDurations.push(eventDirationMillis); - if (event?.['@timestamp']) { - eventDurationsWithTimestamp[event?.['@timestamp']] = eventDirationMillis; - } - } - continue; } @@ -106,6 +100,23 @@ export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams) } } + for (const event of executionEvents.reverse()) { + const timeStamp = event?.['@timestamp']; + if (timeStamp === undefined) continue; + const action = event?.event?.action; + + if (action === undefined) continue; + if (action !== EVENT_LOG_ACTIONS.execute) { + continue; + } + + if (event?.event?.duration) { + const eventDirationMillis = event.event.duration / Millis2Nanos; + eventDurations.push(eventDirationMillis); + eventDurationsWithTimestamp[event['@timestamp']!] = eventDirationMillis; + } + } + // set the muted status of alerts for (const alertId of rule.mutedInstanceIds) { getAlertStatus(alerts, alertId).muted = true; diff --git a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts index 6d10c503422a9..4193e769dc513 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts @@ -79,6 +79,7 @@ describe('getRuleAlertSummaryRoute', () => { Object { "dateStart": undefined, "id": "1", + "numberOfExecutions": undefined, }, ] `); diff --git a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts index fe30454b35a33..097eee875fcee 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts @@ -22,13 +22,16 @@ const paramSchema = schema.object({ const querySchema = schema.object({ date_start: schema.maybe(schema.string()), + number_of_executions: schema.maybe(schema.number()), }); const rewriteReq: RewriteRequestCase = ({ date_start: dateStart, + number_of_executions: numberOfExecutions, ...rest }) => ({ ...rest, + numberOfExecutions, dateStart, }); diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 3603a60baadb1..aa017f91278c1 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -218,6 +218,7 @@ export interface UpdateOptions { export interface GetAlertSummaryParams { id: string; dateStart?: string; + numberOfExecutions?: number; } // NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects @@ -532,7 +533,11 @@ export class RulesClient { } } - public async getAlertSummary({ id, dateStart }: GetAlertSummaryParams): Promise { + public async getAlertSummary({ + id, + dateStart, + numberOfExecutions, + }: GetAlertSummaryParams): Promise { this.logger.debug(`getAlertSummary(): getting alert ${id}`); const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId; @@ -545,7 +550,7 @@ export class RulesClient { // default duration of instance summary is 60 * rule interval const dateNow = new Date(); - const durationMillis = parseDuration(rule.schedule.interval) * 60; + const durationMillis = parseDuration(rule.schedule.interval) * (numberOfExecutions ?? 60); const defaultDateStart = new Date(dateNow.valueOf() - durationMillis); const parsedDateStart = parseDate(dateStart, 'dateStart', defaultDateStart); @@ -553,30 +558,49 @@ export class RulesClient { this.logger.debug(`getAlertSummary(): search the event log for rule ${id}`); let events: IEvent[]; + let executionEvents: IEvent[]; + try { - const queryResults = await eventLogClient.findEventsBySavedObjectIds( - 'alert', - [id], - { - page: 1, - per_page: 10000, - start: parsedDateStart.toISOString(), - end: dateNow.toISOString(), - sort_order: 'desc', - }, - rule.legacyId !== null ? [rule.legacyId] : undefined - ); + const [queryResults, executionResults] = await Promise.all([ + eventLogClient.findEventsBySavedObjectIds( + 'alert', + [id], + { + page: 1, + per_page: 10000, + start: parsedDateStart.toISOString(), + sort_order: 'desc', + end: dateNow.toISOString(), + }, + rule.legacyId !== null ? [rule.legacyId] : undefined + ), + eventLogClient.findEventsBySavedObjectIds( + 'alert', + [id], + { + page: 1, + per_page: numberOfExecutions ?? 60, + filter: 'event.provider: alerting AND event.action:execute', + sort_order: 'desc', + end: dateNow.toISOString(), + }, + rule.legacyId !== null ? [rule.legacyId] : undefined + ), + ]); events = queryResults.data; + executionEvents = executionResults.data; } catch (err) { this.logger.debug( `rulesClient.getAlertSummary(): error searching event log for rule ${id}: ${err.message}` ); events = []; + executionEvents = []; } return alertSummaryFromEventLog({ rule, events, + executionEvents, dateStart: parsedDateStart.toISOString(), dateEnd: dateNow.toISOString(), }); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts index 7a7ab035aa391..81d98eff0297a 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts @@ -135,6 +135,14 @@ describe('getAlertSummary()', () => { }; eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(eventsResult); + const executionEvents = eventsFactory.getEvents(); + const executionEventsResult = { + ...AlertSummaryFindEventsResult, + total: executionEvents.length, + data: executionEvents, + }; + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(executionEventsResult); + const dateStart = new Date(Date.now() - 60 * 1000).toISOString(); const durations: Record = eventsFactory.getExecutionDurations(); @@ -203,7 +211,7 @@ describe('getAlertSummary()', () => { await rulesClient.getAlertSummary({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(2); expect(eventLogClient.findEventsBySavedObjectIds.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alert", @@ -240,7 +248,7 @@ describe('getAlertSummary()', () => { await rulesClient.getAlertSummary({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(2); expect(eventLogClient.findEventsBySavedObjectIds.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alert", @@ -269,7 +277,7 @@ describe('getAlertSummary()', () => { await rulesClient.getAlertSummary({ id: '1', dateStart }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(2); const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!; expect({ start, end }).toMatchInlineSnapshot(` @@ -288,7 +296,7 @@ describe('getAlertSummary()', () => { await rulesClient.getAlertSummary({ id: '1', dateStart }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(2); const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!; expect({ start, end }).toMatchInlineSnapshot(` @@ -299,6 +307,25 @@ describe('getAlertSummary()', () => { `); }); + test('calls event log client with number of executions', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject()); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); + + const numberOfExecutions = 15; + await rulesClient.getAlertSummary({ id: '1', numberOfExecutions }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(2); + const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[1][2]!; + + expect({ start, end }).toMatchInlineSnapshot(` + Object { + "end": "2019-02-12T21:01:22.479Z", + "start": undefined, + } + `); + }); + test('invalid start date throws an error', async () => { unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject()); eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4c4910cede8e7..7cce3bbf6a410 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -26073,7 +26073,6 @@ "xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText": "「{connectorName}」を更新しました", "xpack.triggersActionsUI.sections.executionDurationChart.executionDurationNoData": "このルールの実行がありません。", "xpack.triggersActionsUI.sections.executionDurationChart.recentDurationsTitle": "最近の実行時間", - "xpack.triggersActionsUI.sections.executionDurationChart.recentDurationsTooltip": "最近のルール実行には、前回の{numExecutions}実行までが含まれています。", "xpack.triggersActionsUI.sections.manageLicense.manageLicenseCancelButtonText": "キャンセル", "xpack.triggersActionsUI.sections.manageLicense.manageLicenseConfirmButtonText": "ライセンスの管理", "xpack.triggersActionsUI.sections.manageLicense.manageLicenseMessage": "ルールタイプ{alertTypeId}は無効です。{licenseRequired}ライセンスが必要です。アップグレードオプションを表示するには、[ライセンス管理]に移動してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8e4a9aaf86138..b3c8ef78be31c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -26521,7 +26521,6 @@ "xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText": "已更新“{connectorName}”", "xpack.triggersActionsUI.sections.executionDurationChart.executionDurationNoData": "此规则没有可用执行。", "xpack.triggersActionsUI.sections.executionDurationChart.recentDurationsTitle": "最近执行持续时间", - "xpack.triggersActionsUI.sections.executionDurationChart.recentDurationsTooltip": "最近规则执行最多包括最后 {numExecutions} 个执行。", "xpack.triggersActionsUI.sections.manageLicense.manageLicenseCancelButtonText": "取消", "xpack.triggersActionsUI.sections.manageLicense.manageLicenseConfirmButtonText": "管理许可证", "xpack.triggersActionsUI.sections.manageLicense.manageLicenseMessage": "规则类型 {alertTypeId} 已禁用,因为它需要{licenseRequired}许可证。继续前往“许可证管理”查看升级选项。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts index 42044ef5e4df1..7a1fdbe53c7c5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts @@ -60,6 +60,11 @@ describe('loadAlertSummary', () => { expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ "/internal/alerting/rule/te%2Fst/_alert_summary", + Object { + "query": Object { + "number_of_executions": undefined, + }, + }, ] `); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts index d11b1191accc5..55ba674dbd7f5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts @@ -40,12 +40,17 @@ const rewriteBodyRes: RewriteRequestCase = ({ export async function loadAlertSummary({ http, ruleId, + numberOfExecutions, }: { http: HttpSetup; ruleId: string; + numberOfExecutions?: number; }): Promise { const res = await http.get>( - `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(ruleId)}/_alert_summary` + `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(ruleId)}/_alert_summary`, + { + query: { number_of_executions: numberOfExecutions }, + } ); return rewriteBodyRes(res); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 7694f289963a3..b92b4289ea905 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -57,6 +57,7 @@ const mockAlertApis = { enableAlert: jest.fn(), disableAlert: jest.fn(), requestRefresh: jest.fn(), + refreshToken: Date.now(), }; const authorizedConsumers = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 5b6ddc1b3345c..1bb979ee86052 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -50,6 +50,7 @@ export type AlertDetailsProps = { alertType: RuleType; actionTypes: ActionType[]; requestRefresh: () => Promise; + refreshToken?: number; } & Pick; export const AlertDetails: React.FunctionComponent = ({ @@ -61,6 +62,7 @@ export const AlertDetails: React.FunctionComponent = ({ unmuteAlert, muteAlert, requestRefresh, + refreshToken, }) => { const history = useHistory(); const { @@ -443,6 +445,7 @@ export const AlertDetails: React.FunctionComponent = ({ {alert.enabled ? ( alertType={alertType} actionTypes={actionTypes} requestRefresh={async () => requestRefresh(Date.now())} + refreshToken={refreshToken} /> ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.test.tsx index fb8892ece8b68..075f6dedeb06a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.test.tsx @@ -24,6 +24,8 @@ const mockAPIs = { muteAlertInstance: jest.fn(), unmuteAlertInstance: jest.fn(), requestRefresh: jest.fn(), + numberOfExecutions: 60, + onChangeDuration: jest.fn(), }; beforeAll(() => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.tsx index 59a0adea64fed..9d4ef14dec64a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.tsx @@ -53,7 +53,10 @@ type AlertsProps = { readOnly: boolean; alertSummary: AlertSummary; requestRefresh: () => Promise; + numberOfExecutions: number; + onChangeDuration: (length: number) => void; durationEpoch?: number; + isLoadingChart?: boolean; } & Pick; export const alertsTableColumns = ( @@ -155,7 +158,10 @@ export function Alerts({ muteAlertInstance, unmuteAlertInstance, requestRefresh, + numberOfExecutions, + onChangeDuration, durationEpoch = Date.now(), + isLoadingChart, }: AlertsProps) { const [pagination, setPagination] = useState({ index: 0, @@ -257,7 +263,12 @@ export function Alerts({ - + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.test.tsx index 36b73bbbf82c0..e0b2ac6c2e6c8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.test.tsx @@ -49,7 +49,7 @@ describe('getAlertState useEffect handler', () => { await getAlertSummary(rule.id, loadAlertSummary, setAlertSummary, toastNotifications); - expect(loadAlertSummary).toHaveBeenCalledWith(rule.id); + expect(loadAlertSummary).toHaveBeenCalledWith(rule.id, undefined); expect(setAlertSummary).toHaveBeenCalledWith(alertSummary); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.tsx index e55181d9b1a04..07e45c8d2b2d0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ToastsApi } from 'kibana/public'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Rule, AlertSummary, RuleType } from '../../../../types'; import { ComponentOpts as AlertApis, @@ -22,6 +22,7 @@ type WithAlertSummaryProps = { ruleType: RuleType; readOnly: boolean; requestRefresh: () => Promise; + refreshToken?: number; } & Pick; export const AlertsRoute: React.FunctionComponent = ({ @@ -30,17 +31,48 @@ export const AlertsRoute: React.FunctionComponent = ({ readOnly, requestRefresh, loadAlertSummary: loadAlertSummary, + refreshToken, }) => { const { notifications: { toasts }, } = useKibana().services; const [alertSummary, setAlertSummary] = useState(null); + const [numberOfExecutions, setNumberOfExecutions] = useState(60); + const [isLoadingChart, setIsLoadingChart] = useState(true); + const ruleID = useRef(null); + const refreshTokenRef = useRef(refreshToken); + + const getAlertSummaryWithLoadingState = useCallback( + async (executions: number = numberOfExecutions) => { + setIsLoadingChart(true); + await getAlertSummary(ruleID.current!, loadAlertSummary, setAlertSummary, toasts, executions); + setIsLoadingChart(false); + }, + [setIsLoadingChart, ruleID, loadAlertSummary, setAlertSummary, toasts, numberOfExecutions] + ); + + useEffect(() => { + if (ruleID.current !== rule.id) { + ruleID.current = rule.id; + getAlertSummaryWithLoadingState(); + } + }, [rule, ruleID, getAlertSummaryWithLoadingState]); useEffect(() => { - getAlertSummary(rule.id, loadAlertSummary, setAlertSummary, toasts); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rule]); + if (refreshTokenRef.current !== refreshToken) { + refreshTokenRef.current = refreshToken; + getAlertSummaryWithLoadingState(); + } + }, [refreshToken, refreshTokenRef, getAlertSummaryWithLoadingState]); + + const onChangeDuration = useCallback( + (executions: number) => { + setNumberOfExecutions(executions); + getAlertSummaryWithLoadingState(executions); + }, + [getAlertSummaryWithLoadingState] + ); return alertSummary ? ( = ({ ruleType={ruleType} readOnly={readOnly} alertSummary={alertSummary} + numberOfExecutions={numberOfExecutions} + isLoadingChart={isLoadingChart} + onChangeDuration={onChangeDuration} /> ) : ( @@ -59,10 +94,11 @@ export async function getAlertSummary( ruleId: string, loadAlertSummary: AlertApis['loadAlertSummary'], setAlertSummary: React.Dispatch>, - toasts: Pick + toasts: Pick, + executionDuration?: number ) { try { - const loadedSummary = await loadAlertSummary(ruleId); + const loadedSummary = await loadAlertSummary(ruleId, executionDuration); setAlertSummary(loadedSummary); } catch (e) { toasts.addDanger({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.test.tsx index 9d32dd27cd2f4..f524ff4426b6a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.test.tsx @@ -14,7 +14,13 @@ describe('execution duration chart', () => { it('renders empty state when no execution duration values are available', async () => { const executionDuration = mockExecutionDuration(); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + {}} + /> + ); await act(async () => { await nextTick(); @@ -32,7 +38,13 @@ describe('execution duration chart', () => { valuesWithTimestamp: { '17 Nov 2021 @ 19:19:17': 1, '17 Nov 2021 @ 20:19:17': 2 }, }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + {}} + /> + ); await act(async () => { await nextTick(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx index d74c1c52e8b07..6cf5e172ff284 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx @@ -6,15 +6,16 @@ */ import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiEmptyPrompt, - EuiIconTip, EuiTitle, + EuiSelect, + EuiLoadingChart, } from '@elastic/eui'; import { euiLightVars as lightEuiTheme } from '@kbn/ui-shared-deps-src/theme'; import { Axis, BarSeries, Chart, CurveType, LineSeries, Settings } from '@elastic/charts'; @@ -27,16 +28,38 @@ export interface ComponentOpts { average: number; valuesWithTimestamp: Record; }; + numberOfExecutions: number; + onChangeDuration: (length: number) => void; + isLoading?: boolean; } -const DESIRED_NUM_EXECUTION_DURATIONS = 30; +const NUM_EXECUTIONS_OPTIONS = [120, 60, 30, 15].map((value) => ({ + value, + text: i18n.translate( + 'xpack.triggersActionsUI.sections.executionDurationChart.numberOfExecutionsOption', + { + defaultMessage: '{value} executions', + values: { + value, + }, + } + ), +})); export const ExecutionDurationChart: React.FunctionComponent = ({ executionDuration, + numberOfExecutions, + onChangeDuration, + isLoading, }: ComponentOpts) => { const paddedExecutionDurations = padOrTruncateDurations( executionDuration.valuesWithTimestamp, - DESIRED_NUM_EXECUTION_DURATIONS + numberOfExecutions + ); + + const onChange = useCallback( + ({ target }) => onChangeDuration(Number(target.value)), + [onChangeDuration] ); return ( @@ -52,92 +75,99 @@ export const ExecutionDurationChart: React.FunctionComponent = ({ - - - - - - {executionDuration.valuesWithTimestamp && - Object.entries(executionDuration.valuesWithTimestamp).length > 0 ? ( - <> - - - [ - timestamp ? moment(timestamp).format('D MMM YYYY @ HH:mm:ss') : ndx, - val, - ])} - minBarHeight={2} - /> - + + [ - timestamp ? moment(timestamp).format('D MMM YYYY @ HH:mm:ss') : ndx, - val ? executionDuration.average : null, - ])} - curve={CurveType.CURVE_NATURAL} + onChange={onChange} /> - formatMillisForDisplay(d)} /> - - - ) : ( - <> - -

- -

- - } - /> - + + + + {isLoading && ( + + + + + )} + {!isLoading && + (executionDuration.valuesWithTimestamp && + Object.entries(executionDuration.valuesWithTimestamp).length > 0 ? ( + <> + + + [ + timestamp ? moment(timestamp).format('D MMM YYYY @ HH:mm:ss') : ndx, + val, + ])} + minBarHeight={2} + /> + [ + timestamp ? moment(timestamp).format('D MMM YYYY @ HH:mm:ss') : ndx, + val ? executionDuration.average : null, + ])} + curve={CurveType.CURVE_NATURAL} + /> + formatMillisForDisplay(d)} /> + + + ) : ( + <> + +

+ +

+ + } + /> + + ))} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx index 1a4c3ad5f4df9..0308e17ac342c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx @@ -57,7 +57,7 @@ export interface ComponentOpts { }>; loadAlert: (id: Rule['id']) => Promise; loadAlertState: (id: Rule['id']) => Promise; - loadAlertSummary: (id: Rule['id']) => Promise; + loadAlertSummary: (id: Rule['id'], numberOfExecutions?: number) => Promise; loadAlertTypes: () => Promise; getHealth: () => Promise; resolveRule: (id: Rule['id']) => Promise; @@ -127,7 +127,9 @@ export function withBulkAlertOperations( deleteAlert={async (alert: Rule) => deleteAlerts({ http, ids: [alert.id] })} loadAlert={async (alertId: Rule['id']) => loadAlert({ http, alertId })} loadAlertState={async (alertId: Rule['id']) => loadAlertState({ http, alertId })} - loadAlertSummary={async (ruleId: Rule['id']) => loadAlertSummary({ http, ruleId })} + loadAlertSummary={async (ruleId: Rule['id'], numberOfExecutions?: number) => + loadAlertSummary({ http, ruleId, numberOfExecutions }) + } loadAlertTypes={async () => loadAlertTypes({ http })} resolveRule={async (ruleId: Rule['id']) => resolveRule({ http, ruleId })} getHealth={async () => alertingFrameworkHealth({ http })} From 6d07ea57355e6a69f0beaca55cd81e4d691ff516 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 20 Jan 2022 14:42:36 -0700 Subject: [PATCH 26/43] [Metrics UI] Rewrite the data fetching for Inventory Threshold rule (#123095) * [Metrics UI] Rewrite the data fetching for Inventory Threshold rule * Adding support for rate aggregations with interfaces * Fixing unused deps * Adding index options to request * removing ts-ignores * Fixing logRate aggregation * Removing unused deps * Updating data * fixing tests with new data * Adding tests for network traffic and log rate * removing todo comment * Adding tests for Pods network traffic * Change after key to use correct nextAfterKey value --- .../infra/common/inventory_models/types.ts | 2 +- .../evaluate_condition.ts | 101 +- .../lib/calculate_from_based_on_metric.ts | 35 + .../lib/calculate_rate_timeranges.ts | 30 + .../lib/create_log_rate_aggs.ts | 22 + .../lib/create_metric_aggregations.ts | 59 + .../lib/create_rate_agg_with_interface.ts | 69 + .../lib/create_rate_aggs.ts | 50 + .../lib/create_request.ts | 71 + .../lib/get_data.ts | 92 + .../inventory_metric_threshold/lib/is_rate.ts | 32 + .../metric_threshold/lib/evaluate_rule.ts | 1 - .../apis/metrics_ui/constants.ts | 8 +- .../metrics_ui/inventory_threshold_alert.ts | 378 +- .../apis/metrics_ui/metrics_alerting.ts | 4 - .../infra/8.0.0/hosts_only/data.json.gz | Bin 345 -> 4926 bytes .../infra/8.0.0/hosts_only/mappings.json | 15301 +--------------- .../infra/8.0.0/pods_only/data.json.gz | Bin 0 -> 7906 bytes .../infra/8.0.0/pods_only/mappings.json | 247 + 19 files changed, 1113 insertions(+), 15389 deletions(-) create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/calculate_from_based_on_metric.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/calculate_rate_timeranges.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_log_rate_aggs.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_metric_aggregations.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_rate_agg_with_interface.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_rate_aggs.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_request.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/is_rate.ts create mode 100644 x-pack/test/functional/es_archives/infra/8.0.0/pods_only/data.json.gz create mode 100644 x-pack/test/functional/es_archives/infra/8.0.0/pods_only/mappings.json diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 35fd7a13a4084..2d4348ddde5de 100644 --- a/x-pack/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/plugins/infra/common/inventory_models/types.ts @@ -252,7 +252,7 @@ export const ESCaridnalityAggRT = rt.type({ export const ESBucketScriptAggRT = rt.type({ bucket_script: rt.intersection([ rt.type({ - buckets_path: rt.record(rt.string, rt.union([rt.undefined, rt.string])), + buckets_path: rt.record(rt.string, rt.string), script: rt.type({ source: rt.string, lang: rt.keyof({ painless: null, expression: null }), diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index b78c5eb291adb..8224c95087339 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -5,25 +5,16 @@ * 2.0. */ -import { mapValues, last, first } from 'lodash'; +import { mapValues } from 'lodash'; import moment from 'moment'; import { ElasticsearchClient } from 'kibana/server'; -import { - isTooManyBucketsPreviewException, - TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, -} from '../../../../common/alerting/metrics'; -import { InfraDatabaseSearchResponse, CallWithRequestParams } from '../../adapters/framework'; import { Comparator, InventoryMetricConditions } from './types'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; -import { - InfraTimerangeInput, - SnapshotRequest, - SnapshotCustomMetricInput, -} from '../../../../common/http_api'; +import { InfraTimerangeInput } from '../../../../common/http_api'; import { InfraSource } from '../../sources'; -import { UNGROUPED_FACTORY_KEY } from '../common/utils'; -import { getNodes } from '../../../routes/snapshot/lib/get_nodes'; import { LogQueryFields } from '../../../services/log_queries/get_log_query_fields'; +import { calcualteFromBasedOnMetric } from './lib/calculate_from_based_on_metric'; +import { getData } from './lib/get_data'; type ConditionResult = InventoryMetricConditions & { shouldFire: boolean[]; @@ -61,10 +52,11 @@ export const evaluateCondition = async ({ const timerange = { to: to.valueOf(), - from: to.clone().subtract(condition.timeSize, condition.timeUnit).valueOf(), + from: calcualteFromBasedOnMetric(to, condition, nodeType, metric, customMetric), interval: `${condition.timeSize}${condition.timeUnit}`, forceInterval: true, } as InfraTimerangeInput; + if (lookbackSize) { timerange.lookbackSize = lookbackSize; } @@ -87,18 +79,15 @@ export const evaluateCondition = async ({ const valueEvaluator = (value?: DataValue, t?: number[], c?: Comparator) => { if (value === undefined || value === null || !t || !c) return [false]; const comparisonFunction = comparatorMap[c]; - return Array.isArray(value) - ? value.map((v) => comparisonFunction(Number(v), t)) - : [comparisonFunction(value as number, t)]; + return [comparisonFunction(value as number, t)]; }; const result = mapValues(currentValues, (value) => { - if (isTooManyBucketsPreviewException(value)) throw value; return { ...condition, shouldFire: valueEvaluator(value, threshold, comparator), shouldWarn: valueEvaluator(value, warningThreshold, warningComparator), - isNoData: Array.isArray(value) ? value.map((v) => v === null) : [value === null], + isNoData: [value === null], isError: value === undefined, currentValue: getCurrentValue(value), }; @@ -107,82 +96,12 @@ export const evaluateCondition = async ({ return result as Record; }; -const getCurrentValue: (value: any) => number = (value) => { - if (Array.isArray(value)) return getCurrentValue(last(value)); +const getCurrentValue: (value: number | null) => number = (value) => { if (value !== null) return Number(value); return NaN; }; -type DataValue = number | null | Array; -const getData = async ( - esClient: ElasticsearchClient, - nodeType: InventoryItemType, - metric: SnapshotMetricType, - timerange: InfraTimerangeInput, - source: InfraSource, - logQueryFields: LogQueryFields | undefined, - compositeSize: number, - filterQuery?: string, - customMetric?: SnapshotCustomMetricInput -) => { - const client = async ( - options: CallWithRequestParams - ): Promise> => - // @ts-expect-error SearchResponse.body.timeout is optional - (await esClient.search(options)).body as InfraDatabaseSearchResponse; - - const metrics = [ - metric === 'custom' ? (customMetric as SnapshotCustomMetricInput) : { type: metric }, - ]; - - const snapshotRequest: SnapshotRequest = { - filterQuery, - nodeType, - groupBy: [], - sourceId: 'default', - metrics, - timerange, - includeTimeseries: Boolean(timerange.lookbackSize), - }; - try { - const { nodes } = await getNodes( - client, - snapshotRequest, - source, - compositeSize, - logQueryFields - ); - - if (!nodes.length) return { [UNGROUPED_FACTORY_KEY]: null }; // No Data state - - return nodes.reduce((acc, n) => { - const { name: nodeName } = n; - const m = first(n.metrics); - if (m && m.value && m.timeseries) { - const { timeseries } = m; - const values = timeseries.rows.map((row) => row.metric_0) as Array; - acc[nodeName] = values; - } else { - acc[nodeName] = m && m.value; - } - return acc; - }, {} as Record | undefined | null>); - } catch (e) { - if (timerange.lookbackSize) { - // This code should only ever be reached when previewing the alert, not executing it - const causedByType = e.body?.error?.caused_by?.type; - if (causedByType === 'too_many_buckets_exception') { - return { - [UNGROUPED_FACTORY_KEY]: { - [TOO_MANY_BUCKETS_PREVIEW_EXCEPTION]: true, - maxBuckets: e.body.error.caused_by.max_buckets, - }, - }; - } - } - return { [UNGROUPED_FACTORY_KEY]: undefined }; - } -}; +type DataValue = number | null; const comparatorMap = { [Comparator.BETWEEN]: (value: number, [a, b]: number[]) => diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/calculate_from_based_on_metric.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/calculate_from_based_on_metric.ts new file mode 100644 index 0000000000000..7c6031dffd57d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/calculate_from_based_on_metric.ts @@ -0,0 +1,35 @@ +/* + * 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 { Moment } from 'moment'; +import { SnapshotCustomMetricInput } from '../../../../../common/http_api'; +import { + InventoryItemType, + SnapshotMetricType, +} from '../../../../../common/inventory_models/types'; +import { InventoryMetricConditions } from '../types'; +import { isRate } from './is_rate'; +import { findInventoryModel } from '../../../../../common/inventory_models'; + +export const calcualteFromBasedOnMetric = ( + to: Moment, + condition: InventoryMetricConditions, + nodeType: InventoryItemType, + metric: SnapshotMetricType, + customMetric?: SnapshotCustomMetricInput +) => { + const inventoryModel = findInventoryModel(nodeType); + const metricAgg = inventoryModel.metrics.snapshot[metric]; + if (isRate(metricAgg, customMetric)) { + return to + .clone() + .subtract(condition.timeSize * 2, condition.timeUnit) + .valueOf(); + } else { + return to.clone().subtract(condition.timeSize, condition.timeUnit).valueOf(); + } +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/calculate_rate_timeranges.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/calculate_rate_timeranges.ts new file mode 100644 index 0000000000000..67a56065289ed --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/calculate_rate_timeranges.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 { InfraTimerangeInput } from '../../../../../common/http_api'; + +export const calculateRateTimeranges = (timerange: InfraTimerangeInput) => { + // This is the total number of milliseconds for the entire timerange + const totalTime = timerange.to - timerange.from; + // Halfway is the to minus half the total time; + const halfway = timerange.to - totalTime / 2; + // The interval is half the total time (divided by 1000 to convert to seconds) + const intervalInSeconds = totalTime / 2000; + + // The first bucket is from the beginning of the time range to the halfway point + const firstBucketRange = { + from: timerange.from, + to: halfway, + }; + + // The second bucket is from the halfway point to the end of the timerange + const secondBucketRange = { + from: halfway, + to: timerange.to, + }; + + return { firstBucketRange, secondBucketRange, intervalInSeconds }; +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_log_rate_aggs.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_log_rate_aggs.ts new file mode 100644 index 0000000000000..4e7e85efb68f7 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_log_rate_aggs.ts @@ -0,0 +1,22 @@ +/* + * 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 { InfraTimerangeInput } from '../../../../../common/http_api'; + +export const createLogRateAggs = (timerange: InfraTimerangeInput, id: string) => { + const intervalInSeconds = (timerange.to - timerange.from) / 1000; + return { + [id]: { + bucket_script: { + buckets_path: { + count: `_count`, + }, + script: `params.count > 0.0 ? params.count / ${intervalInSeconds}: null`, + }, + }, + }; +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_metric_aggregations.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_metric_aggregations.ts new file mode 100644 index 0000000000000..6d516fa54b5f1 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_metric_aggregations.ts @@ -0,0 +1,59 @@ +/* + * 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 { get } from 'lodash'; +import { + InventoryItemType, + SnapshotMetricType, +} from '../../../../../common/inventory_models/types'; +import { findInventoryModel } from '../../../../../common/inventory_models'; +import { InfraTimerangeInput, SnapshotCustomMetricInput } from '../../../../../common/http_api'; +import { isMetricRate, isCustomMetricRate, isInterfaceRateAgg } from './is_rate'; +import { createRateAggs } from './create_rate_aggs'; +import { createLogRateAggs } from './create_log_rate_aggs'; +import { createRateAggsWithInterface } from './create_rate_agg_with_interface'; + +export const createMetricAggregations = ( + timerange: InfraTimerangeInput, + nodeType: InventoryItemType, + metric: SnapshotMetricType, + customMetric?: SnapshotCustomMetricInput +) => { + const inventoryModel = findInventoryModel(nodeType); + if (customMetric && customMetric.field) { + if (isCustomMetricRate(customMetric)) { + return createRateAggs(timerange, customMetric.id, customMetric.field); + } + return { + [customMetric.id]: { + [customMetric.aggregation]: { + field: customMetric.field, + }, + }, + }; + } else if (metric === 'logRate') { + return createLogRateAggs(timerange, metric); + } else { + const metricAgg = inventoryModel.metrics.snapshot[metric]; + if (isInterfaceRateAgg(metricAgg)) { + const field = get( + metricAgg, + `${metric}_interfaces.aggregations.${metric}_interface_max.max.field` + ) as unknown as string; + const interfaceField = get( + metricAgg, + `${metric}_interfaces.terms.field` + ) as unknown as string; + return createRateAggsWithInterface(timerange, metric, field, interfaceField); + } + if (isMetricRate(metricAgg)) { + const field = get(metricAgg, `${metric}_max.max.field`) as unknown as string; + return createRateAggs(timerange, metric, field); + } + return metricAgg; + } +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_rate_agg_with_interface.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_rate_agg_with_interface.ts new file mode 100644 index 0000000000000..ee58dfac70f52 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_rate_agg_with_interface.ts @@ -0,0 +1,69 @@ +/* + * 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 { InfraTimerangeInput } from '../../../../../common/http_api'; +import { calculateRateTimeranges } from './calculate_rate_timeranges'; + +export const createRateAggsWithInterface = ( + timerange: InfraTimerangeInput, + id: string, + field: string, + interfaceField: string +) => { + const { firstBucketRange, secondBucketRange, intervalInSeconds } = + calculateRateTimeranges(timerange); + + const interfaceAggs = { + interfaces: { + terms: { + field: interfaceField, + }, + aggs: { maxValue: { max: { field } } }, + }, + sumOfInterfaces: { + sum_bucket: { + buckets_path: 'interfaces>maxValue', + }, + }, + }; + + return { + [`${id}_first_bucket`]: { + filter: { + range: { + '@timestamp': { + gte: firstBucketRange.from, + lt: firstBucketRange.to, + format: 'epoch_millis', + }, + }, + }, + aggs: interfaceAggs, + }, + [`${id}_second_bucket`]: { + filter: { + range: { + '@timestamp': { + gte: secondBucketRange.from, + lt: secondBucketRange.to, + format: 'epoch_millis', + }, + }, + }, + aggs: interfaceAggs, + }, + [id]: { + bucket_script: { + buckets_path: { + first: `${id}_first_bucket.sumOfInterfaces`, + second: `${id}_second_bucket.sumOfInterfaces`, + }, + script: `params.second > 0.0 && params.first > 0.0 && params.second > params.first ? (params.second - params.first) / ${intervalInSeconds}: null`, + }, + }, + }; +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_rate_aggs.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_rate_aggs.ts new file mode 100644 index 0000000000000..af786d41fd11d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_rate_aggs.ts @@ -0,0 +1,50 @@ +/* + * 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 { InfraTimerangeInput } from '../../../../../common/http_api'; +import { calculateRateTimeranges } from './calculate_rate_timeranges'; + +export const createRateAggs = (timerange: InfraTimerangeInput, id: string, field: string) => { + const { firstBucketRange, secondBucketRange, intervalInSeconds } = + calculateRateTimeranges(timerange); + + return { + [`${id}_first_bucket`]: { + filter: { + range: { + '@timestamp': { + gte: firstBucketRange.from, + lt: firstBucketRange.to, + format: 'epoch_millis', + }, + }, + }, + aggs: { maxValue: { max: { field } } }, + }, + [`${id}_second_bucket`]: { + filter: { + range: { + '@timestamp': { + gte: secondBucketRange.from, + lt: secondBucketRange.to, + format: 'epoch_millis', + }, + }, + }, + aggs: { maxValue: { max: { field } } }, + }, + [id]: { + bucket_script: { + buckets_path: { + first: `${id}_first_bucket.maxValue`, + second: `${id}_second_bucket.maxValue`, + }, + script: `params.second > 0.0 && params.first > 0.0 && params.second > params.first ? (params.second - params.first) / ${intervalInSeconds}: null`, + }, + }, + }; +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_request.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_request.ts new file mode 100644 index 0000000000000..2472a01f40095 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_request.ts @@ -0,0 +1,71 @@ +/* + * 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 { ESSearchRequest } from 'src/core/types/elasticsearch'; +import { findInventoryFields } from '../../../../../common/inventory_models'; +import { InfraTimerangeInput, SnapshotCustomMetricInput } from '../../../../../common/http_api'; +import { + InventoryItemType, + SnapshotMetricType, +} from '../../../../../common/inventory_models/types'; +import { parseFilterQuery } from '../../../../utils/serialized_query'; +import { createMetricAggregations } from './create_metric_aggregations'; + +export const createRequest = ( + index: string, + nodeType: InventoryItemType, + metric: SnapshotMetricType, + timerange: InfraTimerangeInput, + compositeSize: number, + afterKey: { node: string } | undefined, + filterQuery?: string, + customMetric?: SnapshotCustomMetricInput +) => { + const filters: any[] = [ + { + range: { + '@timestamp': { + gte: timerange.from, + lte: timerange.to, + format: 'epoch_millis', + }, + }, + }, + ]; + const parsedFilters = parseFilterQuery(filterQuery); + if (parsedFilters) { + filters.push(parsedFilters); + } + + const inventoryFields = findInventoryFields(nodeType); + + const composite: any = { + size: compositeSize, + sources: [{ node: { terms: { field: inventoryFields.id } } }], + }; + if (afterKey) { + composite.after = afterKey; + } + const metricAggregations = createMetricAggregations(timerange, nodeType, metric, customMetric); + + const request: ESSearchRequest = { + allow_no_indices: true, + ignore_unavailable: true, + index, + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: { + nodes: { + composite, + aggs: metricAggregations, + }, + }, + }, + }; + + return request; +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts new file mode 100644 index 0000000000000..83751087d9aff --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts @@ -0,0 +1,92 @@ +/* + * 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 { ElasticsearchClient } from 'kibana/server'; +import { InfraTimerangeInput, SnapshotCustomMetricInput } from '../../../../../common/http_api'; +import { + InventoryItemType, + SnapshotMetricType, +} from '../../../../../common/inventory_models/types'; +import { LogQueryFields } from '../../../../services/log_queries/get_log_query_fields'; +import { InfraSource } from '../../../sources'; +import { createRequest } from './create_request'; + +interface BucketKey { + node: string; +} +type Response = Record; +type Metric = Record; +interface Bucket { + key: BucketKey; + doc_count: number; +} +type NodeBucket = Bucket & Metric; +interface ResponseAggregations { + nodes: { + after_key?: BucketKey; + buckets: NodeBucket[]; + }; +} + +export const getData = async ( + esClient: ElasticsearchClient, + nodeType: InventoryItemType, + metric: SnapshotMetricType, + timerange: InfraTimerangeInput, + source: InfraSource, + logQueryFields: LogQueryFields | undefined, + compositeSize: number, + filterQuery?: string, + customMetric?: SnapshotCustomMetricInput, + afterKey?: BucketKey, + previousNodes: Response = {} +): Promise => { + const handleResponse = (aggs: ResponseAggregations, previous: Response) => { + const { nodes } = aggs; + const nextAfterKey = nodes.after_key; + for (const bucket of nodes.buckets) { + const metricId = customMetric && customMetric.field ? customMetric.id : metric; + previous[bucket.key.node] = bucket?.[metricId]?.value ?? null; + } + if (nextAfterKey && nodes.buckets.length === compositeSize) { + return getData( + esClient, + nodeType, + metric, + timerange, + source, + logQueryFields, + compositeSize, + filterQuery, + customMetric, + nextAfterKey, + previous + ); + } + return previous; + }; + + const index = + metric === 'logRate' && logQueryFields + ? logQueryFields.indexPattern + : source.configuration.metricAlias; + const request = createRequest( + index, + nodeType, + metric, + timerange, + compositeSize, + afterKey, + filterQuery, + customMetric + ); + const { body } = await esClient.search(request); + if (body.aggregations) { + return handleResponse(body.aggregations, previousNodes); + } + return previousNodes; +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/is_rate.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/is_rate.ts new file mode 100644 index 0000000000000..b29c6ac71cc50 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/is_rate.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { has } from 'lodash'; +import { MetricsUIAggregation } from '../../../../../common/inventory_models/types'; +import { SnapshotCustomMetricInput } from '../../../../../common/http_api'; + +export const isMetricRate = (metric: MetricsUIAggregation): boolean => { + const values = Object.values(metric); + return values.some((agg) => has(agg, 'derivative')) && values.some((agg) => has(agg, 'max')); +}; + +export const isCustomMetricRate = (customMetric: SnapshotCustomMetricInput) => { + return customMetric.aggregation === 'rate'; +}; + +export const isInterfaceRateAgg = (metric: MetricsUIAggregation) => { + const values = Object.values(metric); + return values.some((agg) => has(agg, 'terms')) && values.some((agg) => has(agg, 'sum_bucket')); +}; + +export const isRate = (metric: MetricsUIAggregation, customMetric?: SnapshotCustomMetricInput) => { + return ( + isMetricRate(metric) || + isInterfaceRateAgg(metric) || + (customMetric && isCustomMetricRate(customMetric)) + ); +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts index 01c8f252fc71b..a3de79368cb80 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts @@ -217,7 +217,6 @@ const getMetric: ( return groupedResults; } const { body: result } = await esClient.search({ - // @ts-expect-error buckets_path is not compatible body: searchBody, index, }); diff --git a/x-pack/test/api_integration/apis/metrics_ui/constants.ts b/x-pack/test/api_integration/apis/metrics_ui/constants.ts index 57963179aa8e4..9ef8c5e6b9407 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/constants.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/constants.ts @@ -19,9 +19,13 @@ export const DATES = { }, }, '8.0.0': { + pods_only: { + min: new Date('2022-01-20T17:09:55.124Z').getTime(), + max: new Date('2022-01-20T17:14:57.378Z').getTime(), + }, hosts_only: { - min: new Date('2022-01-02T00:00:00.000Z').getTime(), - max: new Date('2022-01-02T00:05:30.000Z').getTime(), + min: new Date('2022-01-18T19:57:47.534Z').getTime(), + max: new Date('2022-01-18T20:02:50.043Z').getTime(), }, logs_and_metrics: { min: 1562786660845, diff --git a/x-pack/test/api_integration/apis/metrics_ui/inventory_threshold_alert.ts b/x-pack/test/api_integration/apis/metrics_ui/inventory_threshold_alert.ts index a6e0ce1bc628f..a5f72f1c43b81 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/inventory_threshold_alert.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/inventory_threshold_alert.ts @@ -15,7 +15,10 @@ import { InfraSource } from '../../../../plugins/infra/server/lib/sources'; import { FtrProviderContext } from '../../ftr_provider_context'; import { DATES } from './constants'; import { evaluateCondition } from '../../../../plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition'; -import { InventoryItemType } from '../../../../plugins/infra/common/inventory_models/types'; +import { + InventoryItemType, + SnapshotMetricType, +} from '../../../../plugins/infra/common/inventory_models/types'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -40,7 +43,7 @@ export default function ({ getService }: FtrProviderContext) { type: 'index_pattern', indexPatternId: 'some-test-id', }, - metricAlias: 'metricbeat-*', + metricAlias: 'metrics-*,metricbeat-*', inventoryDefaultView: 'default', metricsExplorerDefaultView: 'default', anomalyThreshold: 70, @@ -78,50 +81,339 @@ export default function ({ getService }: FtrProviderContext) { }; describe('Inventory Threshold Rule Executor', () => { - before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/hosts_only')); - after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/8.0.0/hosts_only')); - it('should work FOR LAST 1 minute', async () => { - const results = await evaluateCondition({ - ...baseOptions, - esClient: convertToKibanaClient(esClient), + describe('CPU per Host', () => { + before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/hosts_only')); + after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/8.0.0/hosts_only')); + it('should work FOR LAST 1 minute', async () => { + const results = await evaluateCondition({ + ...baseOptions, + esClient: convertToKibanaClient(esClient), + }); + expect(results).to.eql({ + 'host-0': { + metric: 'cpu', + timeSize: 1, + timeUnit: 'm', + sourceId: 'default', + threshold: [100], + comparator: '>', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 1.109, + }, + 'host-1': { + metric: 'cpu', + timeSize: 1, + timeUnit: 'm', + sourceId: 'default', + threshold: [100], + comparator: '>', + shouldFire: [false], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 0.7703333333333333, + }, + }); }); - expect(results).to.eql({ - 'host-01': { - metric: 'cpu', - timeSize: 1, - timeUnit: 'm', - sourceId: 'default', - threshold: [100], - comparator: '>', - shouldFire: [true], - shouldWarn: [false], - isNoData: [false], - isError: false, - currentValue: 1.01, - }, + it('should work FOR LAST 5 minute', async () => { + const options = { + ...baseOptions, + condition: { ...baseCondition, timeSize: 5 }, + esClient: convertToKibanaClient(esClient), + }; + const results = await evaluateCondition(options); + expect(results).to.eql({ + 'host-0': { + metric: 'cpu', + timeSize: 5, + timeUnit: 'm', + sourceId: 'default', + threshold: [100], + comparator: '>', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 1.0376666666666665, + }, + 'host-1': { + metric: 'cpu', + timeSize: 5, + timeUnit: 'm', + sourceId: 'default', + threshold: [100], + comparator: '>', + shouldFire: [false], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 0.9192, + }, + }); }); }); - it('should work FOR LAST 5 minute', async () => { - const options = { - ...baseOptions, - condition: { ...baseCondition, timeSize: 5 }, - esClient: convertToKibanaClient(esClient), - }; - const results = await evaluateCondition(options); - expect(results).to.eql({ - 'host-01': { - metric: 'cpu', - timeSize: 5, - timeUnit: 'm', - sourceId: 'default', - threshold: [100], - comparator: '>', - shouldFire: [false], - shouldWarn: [false], - isNoData: [false], - isError: false, - currentValue: 0.24000000000000002, - }, + + describe('Inbound network traffic per host', () => { + before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/hosts_only')); + after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/8.0.0/hosts_only')); + it('should work FOR LAST 1 minute', async () => { + const results = await evaluateCondition({ + ...baseOptions, + condition: { + ...baseCondition, + metric: 'rx', + threshold: [1], + }, + esClient: convertToKibanaClient(esClient), + }); + expect(results).to.eql({ + 'host-0': { + metric: 'rx', + timeSize: 1, + timeUnit: 'm', + sourceId: 'default', + threshold: [1], + comparator: '>', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 1666.6666666666667, + }, + 'host-1': { + metric: 'rx', + timeSize: 1, + timeUnit: 'm', + sourceId: 'default', + threshold: [1], + comparator: '>', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 2000, + }, + }); + }); + it('should work FOR LAST 5 minute', async () => { + const options = { + ...baseOptions, + condition: { + ...baseCondition, + metric: 'rx' as SnapshotMetricType, + threshold: [1], + timeSize: 5, + }, + esClient: convertToKibanaClient(esClient), + }; + const results = await evaluateCondition(options); + expect(results).to.eql({ + 'host-0': { + metric: 'rx', + timeSize: 5, + timeUnit: 'm', + sourceId: 'default', + threshold: [1], + comparator: '>', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 2266.6666666666665, + }, + 'host-1': { + metric: 'rx', + timeSize: 5, + timeUnit: 'm', + sourceId: 'default', + threshold: [1], + comparator: '>', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 2266.6666666666665, + }, + }); + }); + }); + + describe('Log rate per host', () => { + before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/hosts_only')); + after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/8.0.0/hosts_only')); + it('should work FOR LAST 1 minute', async () => { + const results = await evaluateCondition({ + ...baseOptions, + logQueryFields: { indexPattern: 'metricbeat-*' }, + condition: { + ...baseCondition, + metric: 'logRate', + threshold: [1], + }, + esClient: convertToKibanaClient(esClient), + }); + expect(results).to.eql({ + 'host-0': { + metric: 'logRate', + timeSize: 1, + timeUnit: 'm', + sourceId: 'default', + threshold: [1], + comparator: '>', + shouldFire: [false], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 0.3, + }, + 'host-1': { + metric: 'logRate', + timeSize: 1, + timeUnit: 'm', + sourceId: 'default', + threshold: [1], + comparator: '>', + shouldFire: [false], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 0.3, + }, + }); + }); + it('should work FOR LAST 5 minute', async () => { + const options = { + ...baseOptions, + logQueryFields: { indexPattern: 'metricbeat-*' }, + condition: { + ...baseCondition, + metric: 'logRate' as SnapshotMetricType, + threshold: [1], + timeSize: 5, + }, + esClient: convertToKibanaClient(esClient), + }; + const results = await evaluateCondition(options); + expect(results).to.eql({ + 'host-0': { + metric: 'logRate', + timeSize: 5, + timeUnit: 'm', + sourceId: 'default', + threshold: [1], + comparator: '>', + shouldFire: [false], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 0.3, + }, + 'host-1': { + metric: 'logRate', + timeSize: 5, + timeUnit: 'm', + sourceId: 'default', + threshold: [1], + comparator: '>', + shouldFire: [false], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 0.3, + }, + }); + }); + }); + + describe('Network rate per pod', () => { + before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/pods_only')); + after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/8.0.0/pods_only')); + it('should work FOR LAST 1 minute', async () => { + const results = await evaluateCondition({ + ...baseOptions, + startTime: DATES['8.0.0'].pods_only.max, + nodeType: 'pod' as InventoryItemType, + condition: { + ...baseCondition, + metric: 'rx', + threshold: [1], + }, + esClient: convertToKibanaClient(esClient), + }); + expect(results).to.eql({ + '7d6d7955-f853-42b1-8613-11f52d0d2725': { + metric: 'rx', + timeSize: 1, + timeUnit: 'm', + sourceId: 'default', + threshold: [1], + comparator: '>', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 43332.833333333336, + }, + 'ed01e3a3-4787-42f6-b73e-ac9e97294e9d': { + metric: 'rx', + timeSize: 1, + timeUnit: 'm', + sourceId: 'default', + threshold: [1], + comparator: '>', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 42783.833333333336, + }, + }); + }); + it('should work FOR LAST 5 minute', async () => { + const results = await evaluateCondition({ + ...baseOptions, + startTime: DATES['8.0.0'].pods_only.max, + logQueryFields: { indexPattern: 'metricbeat-*' }, + nodeType: 'pod', + condition: { + ...baseCondition, + metric: 'rx', + threshold: [1], + timeSize: 5, + }, + esClient: convertToKibanaClient(esClient), + }); + expect(results).to.eql({ + '7d6d7955-f853-42b1-8613-11f52d0d2725': { + metric: 'rx', + timeSize: 5, + timeUnit: 'm', + sourceId: 'default', + threshold: [1], + comparator: '>', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 50197.666666666664, + }, + 'ed01e3a3-4787-42f6-b73e-ac9e97294e9d': { + metric: 'rx', + timeSize: 5, + timeUnit: 'm', + sourceId: 'default', + threshold: [1], + comparator: '>', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + currentValue: 50622.066666666666, + }, + }); }); }); }); diff --git a/x-pack/test/api_integration/apis/metrics_ui/metrics_alerting.ts b/x-pack/test/api_integration/apis/metrics_ui/metrics_alerting.ts index fd9767e13d9ef..02a9f3070fe58 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metrics_alerting.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metrics_alerting.ts @@ -40,7 +40,6 @@ export default function ({ getService }: FtrProviderContext) { const searchBody = getElasticsearchMetricQuery(getSearchParams(aggType), timeframe, 100); const result = await client.search({ index, - // @ts-expect-error @elastic/elasticsearch AggregationsBucketsPath is not valid body: searchBody, }); @@ -64,7 +63,6 @@ export default function ({ getService }: FtrProviderContext) { ); const result = await client.search({ index, - // @ts-expect-error @elastic/elasticsearch AggregationsBucketsPath is not valid body: searchBody, }); @@ -87,7 +85,6 @@ export default function ({ getService }: FtrProviderContext) { ); const result = await client.search({ index, - // @ts-expect-error @elastic/elasticsearch AggregationsBucketsPath is not valid body: searchBody, }); @@ -109,7 +106,6 @@ export default function ({ getService }: FtrProviderContext) { ); const result = await client.search({ index, - // @ts-expect-error @elastic/elasticsearch AggregationsBucketsPath is not valid body: searchBody, }); diff --git a/x-pack/test/functional/es_archives/infra/8.0.0/hosts_only/data.json.gz b/x-pack/test/functional/es_archives/infra/8.0.0/hosts_only/data.json.gz index bdd31f088a41ab7c0f81423cdedb4a5d508af7a6..217442c2cf338f2357d487a50558a2f5e7fab604 100644 GIT binary patch literal 4926 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 literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/infra/8.0.0/pods_only/mappings.json b/x-pack/test/functional/es_archives/infra/8.0.0/pods_only/mappings.json new file mode 100644 index 0000000000000..d979f8968937c --- /dev/null +++ b/x-pack/test/functional/es_archives/infra/8.0.0/pods_only/mappings.json @@ -0,0 +1,247 @@ +{ + "type": "index", + "value": { + "aliases": { + "metrics-fake_k8s": { + } + }, + "index": "high-cardinality-data-fake_k8s-2022-01-20", + "mappings": { + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "event": { + "properties": { + "dataset": { + "ignore_above": 256, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "module": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "kubernetes": { + "properties": { + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "cpu": { + "properties": { + "usage": { + "properties": { + "limit": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "nanocores": { + "type": "long" + }, + "node": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + } + } + }, + "host_ip": { + "ignore_above": 256, + "type": "keyword" + }, + "ip": { + "ignore_above": 256, + "type": "keyword" + }, + "memory": { + "properties": { + "usage": { + "properties": { + "available": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "bytes": { + "type": "long" + }, + "limit": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "major_page_faults": { + "type": "long" + }, + "node": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "page_faults": { + "type": "long" + }, + "rss": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "working_set": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "network": { + "properties": { + "rx": { + "properties": { + "bytes": { + "type": "long" + }, + "errors": { + "type": "long" + } + } + }, + "tx": { + "properties": { + "bytes": { + "type": "long" + }, + "errors": { + "type": "long" + } + } + } + } + }, + "startTime": { + "type": "date" + }, + "status": { + "properties": { + "phase": { + "ignore_above": 1024, + "type": "keyword" + }, + "ready": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheduled": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "metricset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "period": { + "type": "long" + } + } + }, + "service": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file From 2d3c95c20d014d0c3e21eb7ab72e624d4526fc14 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 20 Jan 2022 22:48:25 +0100 Subject: [PATCH 27/43] [Synthetics Service] Make service keys optional (#123507) --- x-pack/plugins/uptime/common/config.ts | 4 ++-- .../server/lib/synthetics_service/get_service_locations.ts | 7 ++++++- .../rest_api/synthetics_service/get_service_locations.ts | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/uptime/common/config.ts b/x-pack/plugins/uptime/common/config.ts index 3e8c6cc009207..6a2c630d31fae 100644 --- a/x-pack/plugins/uptime/common/config.ts +++ b/x-pack/plugins/uptime/common/config.ts @@ -10,10 +10,10 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { sslSchema } from '@kbn/server-http-tools'; const serviceConfig = schema.object({ - enabled: schema.boolean(), + enabled: schema.maybe(schema.boolean()), username: schema.maybe(schema.string()), password: schema.maybe(schema.string()), - manifestUrl: schema.string(), + manifestUrl: schema.maybe(schema.string()), hosts: schema.maybe(schema.arrayOf(schema.string())), syncInterval: schema.maybe(schema.string()), tls: schema.maybe(sslSchema), diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts index 4802b4687e00d..54686b282bbb4 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts @@ -12,8 +12,13 @@ import { ServiceLocationsApiResponse, } from '../../../common/runtime_types'; -export async function getServiceLocations({ manifestUrl }: { manifestUrl: string }) { +export async function getServiceLocations({ manifestUrl }: { manifestUrl?: string }) { const locations: ServiceLocations = []; + + if (!manifestUrl) { + return { locations }; + } + try { const { data } = await axios.get<{ locations: Record }>(manifestUrl); diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts index 0c51a53e8fd03..e785ec583d8ca 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts @@ -14,5 +14,5 @@ export const getServiceLocationsRoute: UMRestApiRouteFactory = () => ({ path: API_URLS.SERVICE_LOCATIONS, validate: {}, handler: async ({ server }): Promise => - getServiceLocations({ manifestUrl: server.config.service!.manifestUrl }), + getServiceLocations({ manifestUrl: server.config.service!.manifestUrl! }), }); From 94e2e9a85d9d2e529170d5050679f33982834e1b Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 20 Jan 2022 14:49:31 -0700 Subject: [PATCH 28/43] [Reporting] Update references to deprecated internal APIs (#123013) * [Reporting] Update reference to fieldFormats plugin * fix license$ reference * fix type refs * rename index pattern to data view * fix locator.getUrl * fix deprecated reference to security.authc * revert and see * fix security * fix mock router * Revert "revert and see" This reverts commit 655be8f77fbfbcd9c7f0e4814d68b78a3dd037d4. * oops another resolved fixme * fix security in the integration tests * fix authorized_user jest test * Apply suggestions from code review Co-authored-by: Michael Dokolin * lint fix Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Michael Dokolin --- x-pack/plugins/reporting/kibana.json | 1 + .../management/components/ilm_policy_link.tsx | 13 +- .../reporting/public/management/index.ts | 4 +- x-pack/plugins/reporting/server/core.ts | 30 ++-- .../generate_csv/generate_csv.ts | 9 +- x-pack/plugins/reporting/server/plugin.ts | 34 ++-- .../generation_from_jobparams.test.ts | 8 +- .../lib/authorized_user_pre_routing.test.ts | 149 +++++++++++------- .../routes/lib/authorized_user_pre_routing.ts | 5 +- .../reporting/server/routes/lib/get_user.ts | 4 +- .../management/integration_tests/jobs.test.ts | 66 ++++---- x-pack/plugins/reporting/server/services.ts | 4 +- .../create_mock_reportingplugin.ts | 14 +- x-pack/plugins/reporting/server/types.ts | 9 +- 14 files changed, 191 insertions(+), 159 deletions(-) diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index 57cfc25c4b528..e7162d0974de6 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -11,6 +11,7 @@ "configPath": ["xpack", "reporting"], "requiredPlugins": [ "data", + "fieldFormats", "esUiShared", "home", "management", diff --git a/x-pack/plugins/reporting/public/management/components/ilm_policy_link.tsx b/x-pack/plugins/reporting/public/management/components/ilm_policy_link.tsx index dfb884c24e917..3fdc83c341ed9 100644 --- a/x-pack/plugins/reporting/public/management/components/ilm_policy_link.tsx +++ b/x-pack/plugins/reporting/public/management/components/ilm_policy_link.tsx @@ -31,14 +31,11 @@ export const IlmPolicyLink: FunctionComponent = ({ locator, navigateToUrl data-test-subj="ilmPolicyLink" size="xs" onClick={() => { - locator - .getUrl({ - page: 'policy_edit', - policyName: ILM_POLICY_NAME, - }) - .then((url) => { - navigateToUrl(url); - }); + const url = locator.getRedirectUrl({ + page: 'policy_edit', + policyName: ILM_POLICY_NAME, + }); + navigateToUrl(url); }} > {i18nTexts.buttonLabel} diff --git a/x-pack/plugins/reporting/public/management/index.ts b/x-pack/plugins/reporting/public/management/index.ts index 7bd7845051d2c..491e2df445d32 100644 --- a/x-pack/plugins/reporting/public/management/index.ts +++ b/x-pack/plugins/reporting/public/management/index.ts @@ -6,7 +6,7 @@ */ import { ApplicationStart, ToastsSetup } from 'src/core/public'; -import { LicensingPluginSetup } from '../../../licensing/public'; +import { LicensingPluginStart } from '../../../licensing/public'; import { UseIlmPolicyStatusReturn } from '../lib/ilm_policy_status_context'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import { ClientConfigType } from '../plugin'; @@ -15,7 +15,7 @@ import type { SharePluginSetup } from '../shared_imports'; export interface ListingProps { apiClient: ReportingAPIClient; capabilities: ApplicationStart['capabilities']; - license$: LicensingPluginSetup['license$']; // FIXME: license$ is deprecated + license$: LicensingPluginStart['license$']; pollConfig: ClientConfigType['poll']; redirect: ApplicationStart['navigateToApp']; navigateToUrl: ApplicationStart['navigateToUrl']; diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index a29e709b4442d..5c00089afc381 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -8,36 +8,36 @@ import Hapi from '@hapi/hapi'; import * as Rx from 'rxjs'; import { filter, first, map, switchMap, take } from 'rxjs/operators'; -import { +import type { BasePath, IClusterClient, - KibanaRequest, PackageInfo, PluginInitializerContext, SavedObjectsClientContract, SavedObjectsServiceStart, - ServiceStatusLevels, StatusServiceSetup, UiSettingsServiceStart, -} from '../../../../src/core/server'; -import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; -import { IEventLogService } from '../../event_log/server'; -import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import { LicensingPluginStart } from '../../licensing/server'; +} from 'src/core/server'; +import type { PluginStart as DataPluginStart } from 'src/plugins/data/server'; +import type { FieldFormatsStart } from 'src/plugins/field_formats/server'; +import { KibanaRequest, ServiceStatusLevels } from '../../../../src/core/server'; +import type { IEventLogService } from '../../event_log/server'; +import type { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import type { LicensingPluginStart } from '../../licensing/server'; import type { ScreenshotResult, ScreenshottingStart } from '../../screenshotting/server'; -import { SecurityPluginSetup } from '../../security/server'; +import type { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import { DEFAULT_SPACE_ID } from '../../spaces/common/constants'; -import { SpacesPluginSetup } from '../../spaces/server'; -import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; +import type { SpacesPluginSetup } from '../../spaces/server'; +import type { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../common/constants'; import { durationToNumber } from '../common/schema_utils'; -import { ReportingConfig, ReportingSetup } from './'; +import type { ReportingConfig, ReportingSetup } from './'; import { ReportingConfigType } from './config'; import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib'; import { reportingEventLoggerFactory } from './lib/event_logger/logger'; -import { IReport, ReportingStore } from './lib/store'; +import type { IReport, ReportingStore } from './lib/store'; import { ExecuteReportTask, MonitorReportsTask, ReportTaskParams } from './lib/tasks'; -import { ReportingPluginRouter, ScreenshotOptions } from './types'; +import type { ReportingPluginRouter, ScreenshotOptions } from './types'; export interface ReportingInternalSetup { eventLog: IEventLogService; @@ -57,9 +57,11 @@ export interface ReportingInternalStart { uiSettings: UiSettingsServiceStart; esClient: IClusterClient; data: DataPluginStart; + fieldFormats: FieldFormatsStart; licensing: LicensingPluginStart; logger: LevelLogger; screenshotting: ScreenshottingStart; + security?: SecurityPluginStart; taskManager: TaskManagerStartContract; } diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index 08f17fd3acbd1..804fc66c54758 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -13,7 +13,7 @@ import type { Datatable } from 'src/plugins/expressions/server'; import type { Writable } from 'stream'; import type { ReportingConfig } from '../../..'; import type { - IndexPattern, + DataView, ISearchSource, ISearchStartSearchSource, SearchFieldValue, @@ -81,11 +81,7 @@ export class CsvGenerator { private stream: Writable ) {} - private async scan( - index: IndexPattern, - searchSource: ISearchSource, - settings: CsvExportSettings - ) { + private async scan(index: DataView, searchSource: ISearchSource, settings: CsvExportSettings) { const { scroll: scrollSettings, includeFrozen } = settings; const searchBody = searchSource.getSearchRequestBody(); this.logger.debug(`executing search request`); @@ -432,7 +428,6 @@ export class CsvGenerator { this.logger.debug(`Finished generating. Row count: ${this.csvRowCount}.`); - // FIXME: https://github.com/elastic/kibana/issues/112186 -- find root cause if (!this.maxSizeReached && this.csvRowCount !== totalRecords) { this.logger.warning( `ES scroll returned fewer total hits than expected! ` + diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 32285772d0e23..3b25fedd0d5fb 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -38,7 +38,6 @@ export class ReportingPlugin public setup(core: CoreSetup, plugins: ReportingSetupDeps) { const { http, status } = core; - const { features, eventLog, security, spaces, taskManager } = plugins; const reportingCore = new ReportingCore(this.logger, this.initContext); @@ -53,22 +52,15 @@ export class ReportingPlugin } }); - const basePath = http.basePath; - const router = http.createRouter(); - reportingCore.pluginSetup({ - status, - features, - eventLog, - security, - spaces, - taskManager, - basePath, - router, logger: this.logger, + status, + basePath: http.basePath, + router: http.createRouter(), + ...plugins, }); - registerEventLogProviderActions(eventLog); + registerEventLogProviderActions(plugins.eventLog); registerUiSettings(core); registerDeprecations({ core, reportingCore }); registerReportingUsageCollector(reportingCore, plugins.usageCollection); @@ -92,27 +84,25 @@ export class ReportingPlugin public start(core: CoreStart, plugins: ReportingStartDeps) { const { elasticsearch, savedObjects, uiSettings } = core; - const { data, licensing, screenshotting, taskManager } = plugins; - // use data plugin for csv formats - setFieldFormats(data.fieldFormats); // FIXME: 'fieldFormats' is deprecated. + + // use fieldFormats plugin for csv formats + setFieldFormats(plugins.fieldFormats); const reportingCore = this.reportingCore!; // async background start (async () => { await reportingCore.pluginSetsUp(); - const store = new ReportingStore(reportingCore, this.logger); + const logger = this.logger; + const store = new ReportingStore(reportingCore, logger); await reportingCore.pluginStart({ - logger: this.logger, + logger, esClient: elasticsearch.client, savedObjects, uiSettings, store, - data, - licensing, - screenshotting, - taskManager, + ...plugins, }); // Note: this must be called after ReportingCore.pluginStart diff --git a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts b/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts index 3ec735083d7cc..f6db9e92086eb 100644 --- a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts +++ b/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts @@ -51,9 +51,6 @@ describe('POST /api/reporting/generate', () => { const mockSetupDeps = createMockPluginSetup({ security: { license: { isEnabled: () => true }, - authc: { - getCurrentUser: () => ({ id: '123', roles: ['superuser'], username: 'Tom Riddle' }), - }, }, router: httpSetup.createRouter(''), }); @@ -64,6 +61,11 @@ describe('POST /api/reporting/generate', () => { ...licensingMock.createStart(), license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'gold' }), }, + security: { + authc: { + getCurrentUser: () => ({ id: '123', roles: ['superuser'], username: 'Tom Riddle' }), + }, + }, }, mockConfigSchema ); diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts index e2ff52036764b..51e0994b59109 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts @@ -8,13 +8,20 @@ import { KibanaRequest, KibanaResponseFactory } from 'src/core/server'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { ReportingCore } from '../../'; -import { ReportingInternalSetup } from '../../core'; -import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; +import { ReportingInternalSetup, ReportingInternalStart } from '../../core'; +import { + createMockConfigSchema, + createMockPluginSetup, + createMockPluginStart, + createMockReportingCore, +} from '../../test_helpers'; import type { ReportingRequestHandlerContext } from '../../types'; import { authorizedUserPreRouting } from './authorized_user_pre_routing'; let mockCore: ReportingCore; -const mockReportingConfig = createMockConfigSchema({ roles: { enabled: false } }); +let mockSetupDeps: ReportingInternalSetup; +let mockStartDeps: ReportingInternalStart; +let mockReportingConfig = createMockConfigSchema({ roles: { enabled: false } }); const getMockContext = () => ({ @@ -36,20 +43,30 @@ const getMockResponseFactory = () => describe('authorized_user_pre_routing', function () { beforeEach(async () => { - mockCore = await createMockReportingCore(mockReportingConfig); + mockSetupDeps = createMockPluginSetup({ + security: { license: { isEnabled: () => true } }, + }); + + mockStartDeps = await createMockPluginStart( + { + security: { + authc: { + getCurrentUser: () => ({ id: '123', roles: ['superuser'], username: 'Tom Riddle' }), + }, + }, + }, + mockReportingConfig + ); + mockCore = await createMockReportingCore(mockReportingConfig, mockSetupDeps, mockStartDeps); }); it('should return from handler with a "false" user when security plugin is not found', async function () { - mockCore.getPluginSetupDeps = () => - ({ - // @ts-ignore - ...mockCore.pluginSetupDeps, - security: undefined, // disable security - } as unknown as ReportingInternalSetup); + mockSetupDeps = createMockPluginSetup({ security: undefined }); + mockCore = await createMockReportingCore(mockReportingConfig, mockSetupDeps, mockStartDeps); const mockResponseFactory = httpServerMock.createResponseFactory() as KibanaResponseFactory; let handlerCalled = false; - authorizedUserPreRouting(mockCore, (user: unknown) => { + await authorizedUserPreRouting(mockCore, (user: unknown) => { expect(user).toBe(false); // verify the user is a false value handlerCalled = true; return Promise.resolve({ status: 200, options: {} }); @@ -59,20 +76,14 @@ describe('authorized_user_pre_routing', function () { }); it('should return from handler with a "false" user when security is disabled', async function () { - mockCore.getPluginSetupDeps = () => - ({ - // @ts-ignore - ...mockCore.pluginSetupDeps, - security: { - license: { - isEnabled: () => false, - }, - }, // disable security - } as unknown as ReportingInternalSetup); + mockSetupDeps = createMockPluginSetup({ + security: { license: { isEnabled: () => false } }, // disable security + }); + mockCore = await createMockReportingCore(mockReportingConfig, mockSetupDeps, mockStartDeps); const mockResponseFactory = httpServerMock.createResponseFactory() as KibanaResponseFactory; let handlerCalled = false; - authorizedUserPreRouting(mockCore, (user: unknown) => { + await authorizedUserPreRouting(mockCore, (user: unknown) => { expect(user).toBe(false); // verify the user is a false value handlerCalled = true; return Promise.resolve({ status: 200, options: {} }); @@ -82,82 +93,104 @@ describe('authorized_user_pre_routing', function () { }); it('should return with 401 when security is enabled and the request is unauthenticated', async function () { - mockCore.getPluginSetupDeps = () => - ({ - // @ts-ignore - ...mockCore.pluginSetupDeps, - security: { - license: { isEnabled: () => true }, - authc: { getCurrentUser: () => null }, - }, - } as unknown as ReportingInternalSetup); + mockSetupDeps = createMockPluginSetup({ + security: { license: { isEnabled: () => true } }, + }); + mockStartDeps = await createMockPluginStart( + { security: { authc: { getCurrentUser: () => null } } }, + mockReportingConfig + ); + mockCore = await createMockReportingCore(mockReportingConfig, mockSetupDeps, mockStartDeps); const mockHandler = () => { throw new Error('Handler callback should not be called'); }; const requestHandler = authorizedUserPreRouting(mockCore, mockHandler); - const mockResponseFactory = getMockResponseFactory(); - expect(requestHandler(getMockContext(), getMockRequest(), mockResponseFactory)).toMatchObject({ + await expect( + requestHandler(getMockContext(), getMockRequest(), getMockResponseFactory()) + ).resolves.toMatchObject({ body: `Sorry, you aren't authenticated`, }); }); describe('Deprecated: security roles for access control', () => { beforeEach(async () => { - const mockReportingConfigDeprecated = createMockConfigSchema({ + mockReportingConfig = createMockConfigSchema({ roles: { allow: ['reporting_user'], enabled: true, }, }); - mockCore = await createMockReportingCore(mockReportingConfigDeprecated); }); it(`should return with 403 when security is enabled but user doesn't have the allowed role`, async function () { - mockCore.getPluginSetupDeps = () => - ({ - // @ts-ignore - ...mockCore.pluginSetupDeps, + mockStartDeps = await createMockPluginStart( + { security: { - license: { isEnabled: () => true }, - authc: { getCurrentUser: () => ({ username: 'friendlyuser', roles: ['cowboy'] }) }, + authc: { + getCurrentUser: () => ({ id: '123', roles: ['peasant'], username: 'Tom Riddle' }), + }, }, - } as unknown as ReportingInternalSetup); - const mockResponseFactory = getMockResponseFactory(); - + }, + mockReportingConfig + ); + mockCore = await createMockReportingCore(mockReportingConfig, mockSetupDeps, mockStartDeps); const mockHandler = () => { throw new Error('Handler callback should not be called'); }; + expect( - authorizedUserPreRouting(mockCore, mockHandler)( + await authorizedUserPreRouting(mockCore, mockHandler)( getMockContext(), getMockRequest(), - mockResponseFactory + getMockResponseFactory() ) ).toMatchObject({ body: `Sorry, you don't have access to Reporting` }); }); - it('should return from handler when security is enabled and user has explicitly allowed role', function (done) { - mockCore.getPluginSetupDeps = () => - ({ - // @ts-ignore - ...mockCore.pluginSetupDeps, + it('should return from handler when security is enabled and user has explicitly allowed role', async function () { + mockStartDeps = await createMockPluginStart( + { security: { - license: { isEnabled: () => true }, authc: { getCurrentUser: () => ({ username: 'friendlyuser', roles: ['reporting_user'] }), }, }, - } as unknown as ReportingInternalSetup); - const mockResponseFactory = getMockResponseFactory(); + }, + mockReportingConfig + ); + mockCore = await createMockReportingCore(mockReportingConfig, mockSetupDeps, mockStartDeps); - authorizedUserPreRouting(mockCore, (user) => { + let handlerCalled = false; + await authorizedUserPreRouting(mockCore, (user: unknown) => { expect(user).toMatchObject({ roles: ['reporting_user'], username: 'friendlyuser' }); - done(); + handlerCalled = true; return Promise.resolve({ status: 200, options: {} }); - })(getMockContext(), getMockRequest(), mockResponseFactory); + })(getMockContext(), getMockRequest(), getMockResponseFactory()); + expect(handlerCalled).toBe(true); }); - it('should return from handler when security is enabled and user has superuser role', async function () {}); + it('should return from handler when security is enabled and user has superuser role', async function () { + mockStartDeps = await createMockPluginStart( + { + security: { + authc: { getCurrentUser: () => ({ username: 'friendlyuser', roles: ['superuser'] }) }, + }, + }, + mockReportingConfig + ); + mockCore = await createMockReportingCore(mockReportingConfig, mockSetupDeps, mockStartDeps); + + const handler = jest.fn().mockResolvedValue({ status: 200, options: {} }); + await authorizedUserPreRouting(mockCore, handler)( + getMockContext(), + getMockRequest(), + getMockResponseFactory() + ); + + expect(handler).toHaveBeenCalled(); + const [[user]] = handler.mock.calls; + expect(user).toMatchObject({ roles: ['superuser'], username: 'friendlyuser' }); + }); }); }); diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts index 2c6a01e615487..7cf7e0b365f26 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts @@ -30,12 +30,13 @@ export const authorizedUserPreRouting = ( ): RequestHandler => { const { logger, security } = reporting.getPluginSetupDeps(); - return (context, req, res) => { + return async (context, req, res) => { + const { security: securityStart } = await reporting.getPluginStartDeps(); try { let user: ReportingRequestUser = false; if (security && security.license.isEnabled()) { // find the authenticated user, or null if security is not enabled - user = getUser(req, security); + user = getUser(req, securityStart); if (!user) { // security is enabled but the user is null return res.unauthorized({ body: `Sorry, you aren't authenticated` }); diff --git a/x-pack/plugins/reporting/server/routes/lib/get_user.ts b/x-pack/plugins/reporting/server/routes/lib/get_user.ts index cc5c97c7a3552..837acf74ba885 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_user.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_user.ts @@ -6,8 +6,8 @@ */ import { KibanaRequest } from 'kibana/server'; -import { SecurityPluginSetup } from '../../../../security/server'; +import { SecurityPluginStart } from '../../../../security/server'; -export function getUser(request: KibanaRequest, security?: SecurityPluginSetup) { +export function getUser(request: KibanaRequest, security?: SecurityPluginStart) { return security?.authc.getCurrentUser(request) ?? false; } diff --git a/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts b/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts index 6cbe7f27fa279..9551f60f8c8e0 100644 --- a/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts @@ -48,6 +48,8 @@ describe('GET /api/reporting/jobs/download', () => { }; }; + const mockConfigSchema = createMockConfigSchema({ roles: { enabled: false } }); + beforeEach(async () => { ({ server, httpSetup } = await setupServer(reportingSymbol)); httpSetup.registerRouteHandlerContext( @@ -56,14 +58,9 @@ describe('GET /api/reporting/jobs/download', () => { () => ({ usesUiCapabilities: jest.fn() }) ); - const mockConfigSchema = createMockConfigSchema({ roles: { enabled: false } }); - mockSetupDeps = createMockPluginSetup({ security: { license: { isEnabled: () => true }, - authc: { - getCurrentUser: () => ({ id: '123', roles: ['superuser'], username: 'Tom Riddle' }), - }, }, router: httpSetup.createRouter(''), }); @@ -74,6 +71,11 @@ describe('GET /api/reporting/jobs/download', () => { ...licensingMock.createStart(), license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'gold' }), }, + security: { + authc: { + getCurrentUser: () => ({ id: '123', roles: ['superuser'], username: 'Tom Riddle' }), + }, + }, }, mockConfigSchema ); @@ -128,19 +130,17 @@ describe('GET /api/reporting/jobs/download', () => { }); it('fails on unauthenticated users', async () => { - // @ts-ignore - core.pluginSetupDeps = { - // @ts-ignore - ...core.pluginSetupDeps, - security: { - license: { - isEnabled: () => true, - }, - authc: { - getCurrentUser: () => undefined, + mockStartDeps = await createMockPluginStart( + { + licensing: { + ...licensingMock.createStart(), + license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'gold' }), }, + security: { authc: { getCurrentUser: () => undefined } }, }, - } as unknown as ReportingInternalSetup; + mockConfigSchema + ); + core = await createMockReportingCore(mockConfigSchema, mockSetupDeps, mockStartDeps); registerJobInfoRoutes(core); await server.start(); @@ -330,25 +330,27 @@ describe('GET /api/reporting/jobs/download', () => { describe('Deprecated: role-based access control', () => { it('fails on users without the appropriate role', async () => { - const deprecatedConfig = createMockConfigSchema({ roles: { enabled: true } }); - core = await createMockReportingCore(deprecatedConfig, mockSetupDeps); - // @ts-ignore - core.pluginSetupDeps = { - // @ts-ignore - ...core.pluginSetupDeps, - security: { - license: { - isEnabled: () => true, + mockStartDeps = await createMockPluginStart( + { + licensing: { + ...licensingMock.createStart(), + license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'gold' }), }, - authc: { - getCurrentUser: () => ({ - id: '123', - roles: ['peasant'], - username: 'Tom Riddle', - }), + security: { + authc: { + getCurrentUser: () => ({ id: '123', roles: ['peasant'], username: 'Tom Riddle' }), + }, }, }, - } as unknown as ReportingInternalSetup; + mockConfigSchema + ); + + core = await createMockReportingCore( + createMockConfigSchema({ roles: { enabled: true } }), + mockSetupDeps, + mockStartDeps + ); + registerJobInfoRoutes(core); await server.start(); diff --git a/x-pack/plugins/reporting/server/services.ts b/x-pack/plugins/reporting/server/services.ts index 2184658865920..55f32e21e4784 100644 --- a/x-pack/plugins/reporting/server/services.ts +++ b/x-pack/plugins/reporting/server/services.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { FieldFormatsStart } from 'src/plugins/field_formats/server'; import { createGetterSetter } from '../../../../src/plugins/kibana_utils/server'; -import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; export const [getFieldFormats, setFieldFormats] = - createGetterSetter('FieldFormats'); + createGetterSetter('FieldFormats'); diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 1f6d7bcff5176..aa065e7be52c7 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -14,6 +14,7 @@ import { coreMock, elasticsearchServiceMock, statusServiceMock } from 'src/core/ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { dataPluginMock } from 'src/plugins/data/server/mocks'; import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; +import { fieldFormatsMock } from 'src/plugins/field_formats/common/mocks'; import { DeepPartial } from 'utility-types'; import { ReportingConfig, ReportingCore } from '../'; import { featuresPluginMock } from '../../../features/server/mocks'; @@ -34,12 +35,12 @@ export const createMockPluginSetup = ( return { features: featuresPluginMock.createSetup(), basePath: { set: jest.fn() }, - router: setupMock.router, + router: { get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn() }, security: securityMock.createSetup(), taskManager: taskManagerMock.createSetup(), logger: createMockLevelLogger(), status: statusServiceMock.createSetupContract(), - eventLog: setupMock.eventLog || { + eventLog: { registerProviderActions: jest.fn(), getLogger: jest.fn(() => ({ logEvent: jest.fn() })), }, @@ -63,9 +64,10 @@ export const createMockPluginStart = async ( ): Promise => { return { esClient: elasticsearchServiceMock.createClusterClient(), - savedObjects: startMock.savedObjects || { getScopedClient: jest.fn() }, - uiSettings: startMock.uiSettings || { asScopedToClient: () => ({ get: jest.fn() }) }, - data: startMock.data || dataPluginMock.createStartContract(), + savedObjects: { getScopedClient: jest.fn() }, + uiSettings: { asScopedToClient: () => ({ get: jest.fn() }) }, + data: dataPluginMock.createStartContract(), + fieldFormats: () => Promise.resolve(fieldFormatsMock), store: await createMockReportingStore(config), taskManager: { schedule: jest.fn().mockImplementation(() => ({ id: 'taskId' })), @@ -76,7 +78,7 @@ export const createMockPluginStart = async ( license$: new BehaviorSubject({ isAvailable: true, isActive: true, type: 'basic' }), }, logger, - screenshotting: startMock.screenshotting || createMockScreenshottingStart(), + screenshotting: createMockScreenshottingStart(), ...startMock, }; }; diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index e558448950c79..c695df6d0a410 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -8,6 +8,7 @@ import type { IRouter, RequestHandlerContext } from 'src/core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { DataPluginStart } from 'src/plugins/data/server/plugin'; +import { FieldFormatsStart } from 'src/plugins/field_formats/server'; import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import type { Writable } from 'stream'; @@ -18,7 +19,11 @@ import type { ScreenshotOptions as BaseScreenshotOptions, ScreenshottingStart, } from '../../screenshotting/server'; -import type { AuthenticatedUser, SecurityPluginSetup } from '../../security/server'; +import type { + AuthenticatedUser, + SecurityPluginSetup, + SecurityPluginStart, +} from '../../security/server'; import type { SpacesPluginSetup } from '../../spaces/server'; import type { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import type { CancellationToken } from '../common/cancellation_token'; @@ -105,8 +110,10 @@ export interface ReportingSetupDeps { */ export interface ReportingStartDeps { data: DataPluginStart; + fieldFormats: FieldFormatsStart; licensing: LicensingPluginStart; screenshotting: ScreenshottingStart; + security?: SecurityPluginStart; taskManager: TaskManagerStartContract; } From 356861d23bd887e5f2d5a2c0f0103a80f59715b8 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 20 Jan 2022 22:04:19 +0000 Subject: [PATCH 29/43] chore(NA): splits types from code on @kbn/ui-shared-deps-src (#123313) * chore(NA): splits types from code on @kbn/ui-shared-deps-src * chore(NA): break flot-charts into another package * chore(NA): skip failing tests * chore(NA): remove skip and add correct value * chore(NA): fix new ui-theme import * chore(NA): adding fleet cloned test * chore(NA): remove cloned test * chore(NA): remove added typo --- .eslintignore | 2 +- .eslintrc.js | 2 +- .i18nrc.json | 2 +- package.json | 4 + packages/BUILD.bazel | 4 + .../elastic-eslint-config-kibana/.eslintrc.js | 2 +- .../flot_charts => kbn-flot-charts}/API.md | 0 packages/kbn-flot-charts/BUILD.bazel | 51 ++++++++ .../flot_charts => kbn-flot-charts}/index.js | 32 +++-- .../lib}/jquery_colorhelpers.js | 0 .../lib}/jquery_flot.js | 0 .../lib}/jquery_flot_axislabels.js | 0 .../lib}/jquery_flot_canvas.js | 0 .../lib}/jquery_flot_categories.js | 0 .../lib}/jquery_flot_crosshair.js | 0 .../lib}/jquery_flot_errorbars.js | 0 .../lib}/jquery_flot_fillbetween.js | 0 .../lib}/jquery_flot_image.js | 0 .../lib}/jquery_flot_log.js | 0 .../lib}/jquery_flot_navigate.js | 0 .../lib}/jquery_flot_pie.js | 0 .../lib}/jquery_flot_resize.js | 0 .../lib}/jquery_flot_selection.js | 0 .../lib}/jquery_flot_stack.js | 0 .../lib}/jquery_flot_symbol.js | 0 .../lib}/jquery_flot_threshold.js | 0 .../lib}/jquery_flot_time.js | 0 packages/kbn-flot-charts/package.json | 7 ++ packages/kbn-optimizer/BUILD.bazel | 2 +- .../src/worker/webpack.config.ts | 2 +- packages/kbn-storybook/BUILD.bazel | 2 +- .../src/lib/run_storybook_cli.ts | 2 +- packages/kbn-ui-shared-deps-src/BUILD.bazel | 32 +++-- .../flot_charts/package.json | 4 - packages/kbn-ui-shared-deps-src/package.json | 3 +- .../src/{index.js => definitions.js} | 13 ++- packages/kbn-ui-shared-deps-src/src/entry.js | 4 +- packages/kbn-ui-shared-deps-src/src/index.ts | 29 +++++ .../kbn-ui-shared-deps-src/theme/package.json | 4 - .../kbn-ui-shared-deps-src/webpack.config.js | 15 +-- packages/kbn-ui-theme/BUILD.bazel | 110 ++++++++++++++++++ packages/kbn-ui-theme/package.json | 8 ++ packages/kbn-ui-theme/src/index.ts | 11 ++ .../src/theme.ts | 0 packages/kbn-ui-theme/tsconfig.json | 14 +++ .../bundle_routes/register_bundle_routes.ts | 2 +- .../bootstrap/get_js_dependency_paths.ts | 2 +- src/core/server/rendering/render_utils.ts | 2 +- .../public/static/components/current_time.tsx | 5 +- .../public/static/components/endzones.tsx | 5 +- .../components/form_fields/type_field.tsx | 2 +- src/plugins/dev_tools/public/application.tsx | 2 +- .../discover_grid_document_selection.tsx | 5 +- .../discover_grid_expand_button.tsx | 5 +- .../discover_grid/get_render_cell_value.tsx | 5 +- .../components/pager/tool_bar_pagination.tsx | 2 +- .../public/react_expression_renderer.tsx | 2 +- .../components/tutorial/instruction_set.js | 2 +- .../public/cluster_configuration_form.tsx | 2 +- .../public/single_chars_field.tsx | 2 +- .../public/use_verification.tsx | 2 +- .../common/eui_styled_components.tsx | 2 +- .../public/code_editor/code_editor_field.tsx | 5 +- .../public/code_editor/editor_theme.ts | 5 +- .../public/data_model/vega_parser.test.js | 2 +- .../vega/public/data_model/vega_parser.ts | 2 +- .../vislib/components/tooltip/tooltip.js | 2 +- x-pack/plugins/apm/common/viz_colors.ts | 2 +- .../plugins/apm/public/application/uxApp.tsx | 2 +- .../percentile_annotations.tsx | 2 +- .../url_filter/url_search/render_option.tsx | 2 +- .../app/service_map/Controls.test.tsx | 2 +- .../public/components/routing/app_root.tsx | 2 +- .../apm/public/utils/httpStatusCodeToColor.ts | 2 +- .../java/gc/fetch_and_transform_gc_metrics.ts | 2 +- .../by_agent/java/gc/get_gc_rate_chart.ts | 2 +- .../by_agent/java/gc/get_gc_time_chart.ts | 2 +- .../by_agent/java/heap_memory/index.ts | 2 +- .../by_agent/java/non_heap_memory/index.ts | 2 +- .../by_agent/java/thread_count/index.ts | 2 +- .../metrics/by_agent/shared/cpu/index.ts | 2 +- .../metrics/fetch_and_transform_metrics.ts | 2 +- .../canvas/shareable_runtime/api/index.ts | 2 +- .../public/common/mock/test_providers.tsx | 2 +- .../components/header_page/index.test.tsx | 2 +- .../utility_bar/utility_bar.test.tsx | 2 +- .../stats_table/hooks/use_color_range.ts | 5 +- .../agents/components/agent_health.tsx | 2 +- .../sections/agents/services/agent_status.tsx | 2 +- .../shared_components/coloring/utils.ts | 2 +- .../xy_visualization/color_assignment.ts | 2 +- .../expression_reference_lines.tsx | 2 +- .../reducers/map/default_map_settings.ts | 2 +- .../ml/common/util/group_color_utils.ts | 2 +- .../color_range_legend/use_color_range.ts | 5 +- .../components/job_messages/job_messages.tsx | 2 +- .../scatterplot_matrix.test.tsx | 2 +- .../scatterplot_matrix_vega_lite_spec.test.ts | 2 +- .../scatterplot_matrix_vega_lite_spec.ts | 2 +- .../get_roc_curve_chart_vega_lite_spec.tsx | 2 +- .../decision_path_chart.tsx | 2 +- .../feature_importance_summary.tsx | 2 +- .../components/charts/common/settings.ts | 5 +- .../core_web_vitals/palette_legends.tsx | 2 +- x-pack/plugins/osquery/public/application.tsx | 2 +- .../public/components/token_field.tsx | 2 +- .../plugins/security/server/prompt_page.tsx | 2 +- .../__examples__/index.stories.tsx | 2 +- .../conditions_table/index.stories.tsx | 2 +- .../viewer/exception_item/index.stories.tsx | 2 +- .../exceptions_viewer_header.stories.tsx | 2 +- .../components/header_page/index.test.tsx | 2 +- .../components/header_section/index.test.tsx | 2 +- .../item_details_card/index.stories.tsx | 2 +- .../text_field_value/index.stories.tsx | 2 +- .../threat_match/logic_buttons.stories.tsx | 2 +- .../utility_bar/utility_bar.test.tsx | 2 +- .../public/common/lib/theme/use_eui_theme.tsx | 5 +- .../public/common/mock/test_providers.tsx | 2 +- .../components/rules/severity_badge/index.tsx | 2 +- .../components/rules/step_about_rule/data.tsx | 2 +- .../common/host_risk_score.test.tsx | 2 +- .../components/common/host_risk_score.tsx | 2 +- .../components/host_score_over_time/index.tsx | 2 +- .../kpi_hosts/risky_hosts/index.tsx | 2 +- .../components/config_form/index.stories.tsx | 2 +- .../trusted_apps_grid/index.stories.tsx | 2 +- .../view_type_toggle/index.stories.tsx | 2 +- .../network/components/details/index.tsx | 5 +- .../map_tool_tip/tooltip_footer.tsx | 2 +- .../components/host_overview/index.tsx | 5 +- .../public/resolver/view/use_colors.ts | 2 +- .../public/resolver/view/use_cube_assets.ts | 2 +- .../body/column_headers/helpers.test.tsx | 2 +- .../t_grid/body/column_headers/helpers.tsx | 2 +- .../components/t_grid/body/constants.ts | 2 +- .../components/t_grid/body/helpers.test.tsx | 2 +- .../t_grid/integrated/index.test.tsx | 2 +- .../timelines/public/mock/test_providers.tsx | 2 +- .../expanded_row_messages_pane.tsx | 2 +- .../components/execution_duration_chart.tsx | 2 +- .../public/contexts/uptime_theme_context.tsx | 2 +- yarn.lock | 16 +++ 143 files changed, 417 insertions(+), 193 deletions(-) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts}/API.md (100%) create mode 100644 packages/kbn-flot-charts/BUILD.bazel rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts}/index.js (62%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_colorhelpers.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_axislabels.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_canvas.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_categories.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_crosshair.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_errorbars.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_fillbetween.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_image.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_log.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_navigate.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_pie.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_resize.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_selection.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_stack.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_symbol.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_threshold.js (100%) rename packages/{kbn-ui-shared-deps-src/src/flot_charts => kbn-flot-charts/lib}/jquery_flot_time.js (100%) create mode 100755 packages/kbn-flot-charts/package.json delete mode 100644 packages/kbn-ui-shared-deps-src/flot_charts/package.json rename packages/kbn-ui-shared-deps-src/src/{index.js => definitions.js} (89%) create mode 100644 packages/kbn-ui-shared-deps-src/src/index.ts delete mode 100644 packages/kbn-ui-shared-deps-src/theme/package.json create mode 100644 packages/kbn-ui-theme/BUILD.bazel create mode 100644 packages/kbn-ui-theme/package.json create mode 100644 packages/kbn-ui-theme/src/index.ts rename packages/{kbn-ui-shared-deps-src => kbn-ui-theme}/src/theme.ts (100%) create mode 100644 packages/kbn-ui-theme/tsconfig.json diff --git a/.eslintignore b/.eslintignore index 5ae3fe7b0967d..7b9b7f77e8379 100644 --- a/.eslintignore +++ b/.eslintignore @@ -35,7 +35,7 @@ snapshots.js /packages/kbn-test/src/functional_test_runner/__tests__/fixtures/ /packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/ /packages/kbn-ui-framework/dist -/packages/kbn-ui-shared-deps-src/src/flot_charts +/packages/kbn-flot-charts/lib /packages/kbn-monaco/src/painless/antlr # Bazel diff --git a/.eslintrc.js b/.eslintrc.js index b303a9fefb691..5ee75d186eb24 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1569,7 +1569,7 @@ module.exports = { }, }, { - files: ['packages/kbn-ui-shared-deps-src/src/flot_charts/**/*.js'], + files: ['packages/kbn-flot-charts/lib/**/*.js'], env: { jquery: true, }, diff --git a/.i18nrc.json b/.i18nrc.json index 4331c170d141f..043e1e28a0e9d 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -32,7 +32,7 @@ "expressionShape": "src/plugins/expression_shape", "expressionTagcloud": "src/plugins/chart_expressions/expression_tagcloud", "fieldFormats": "src/plugins/field_formats", - "flot": "packages/kbn-ui-shared-deps-src/src/flot_charts", + "flot": "packages/kbn-flot-charts/lib", "home": "src/plugins/home", "indexPatternEditor": "src/plugins/data_view_editor", "indexPatternFieldEditor": "src/plugins/data_view_field_editor", diff --git a/package.json b/package.json index 0bd1b5c9e5b55..4e7044b458a44 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto", "@kbn/es-query": "link:bazel-bin/packages/kbn-es-query", "@kbn/field-types": "link:bazel-bin/packages/kbn-field-types", + "@kbn/flot-charts": "link:bazel-bin/packages/kbn-flot-charts", "@kbn/i18n": "link:bazel-bin/packages/kbn-i18n", "@kbn/i18n-react": "link:bazel-bin/packages/kbn-i18n-react", "@kbn/interpreter": "link:bazel-bin/packages/kbn-interpreter", @@ -169,6 +170,7 @@ "@kbn/ui-framework": "link:bazel-bin/packages/kbn-ui-framework", "@kbn/ui-shared-deps-npm": "link:bazel-bin/packages/kbn-ui-shared-deps-npm", "@kbn/ui-shared-deps-src": "link:bazel-bin/packages/kbn-ui-shared-deps-src", + "@kbn/ui-theme": "link:bazel-bin/packages/kbn-ui-theme", "@kbn/utility-types": "link:bazel-bin/packages/kbn-utility-types", "@kbn/utils": "link:bazel-bin/packages/kbn-utils", "@loaders.gl/core": "^2.3.1", @@ -602,6 +604,8 @@ "@types/kbn__std": "link:bazel-bin/packages/kbn-std/npm_module_types", "@types/kbn__telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools/npm_module_types", "@types/kbn__ui-shared-deps-npm": "link:bazel-bin/packages/kbn-ui-shared-deps-npm/npm_module_types", + "@types/kbn__ui-shared-deps-src": "link:bazel-bin/packages/kbn-ui-shared-deps-src/npm_module_types", + "@types/kbn__ui-theme": "link:bazel-bin/packages/kbn-ui-theme/npm_module_types", "@types/kbn__utility-types": "link:bazel-bin/packages/kbn-utility-types/npm_module_types", "@types/kbn__utils": "link:bazel-bin/packages/kbn-utils/npm_module_types", "@types/license-checker": "15.0.0", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 7c8259b2f6857..be4d1087bc21a 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -26,6 +26,7 @@ filegroup( "//packages/kbn-eslint-plugin-eslint:build", "//packages/kbn-expect:build", "//packages/kbn-field-types:build", + "//packages/kbn-flot-charts:build", "//packages/kbn-i18n:build", "//packages/kbn-i18n-react:build", "//packages/kbn-interpreter:build", @@ -66,6 +67,7 @@ filegroup( "//packages/kbn-ui-framework:build", "//packages/kbn-ui-shared-deps-npm:build", "//packages/kbn-ui-shared-deps-src:build", + "//packages/kbn-ui-theme:build", "//packages/kbn-utility-types:build", "//packages/kbn-utils:build", ], @@ -121,6 +123,8 @@ filegroup( "//packages/kbn-std:build_types", "//packages/kbn-telemetry-tools:build_types", "//packages/kbn-ui-shared-deps-npm:build_types", + "//packages/kbn-ui-shared-deps-src:build_types", + "//packages/kbn-ui-theme:build_types", "//packages/kbn-utility-types:build_types", "//packages/kbn-utils:build_types", ], diff --git a/packages/elastic-eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js index fe6ea54bbde05..8193380d662d8 100644 --- a/packages/elastic-eslint-config-kibana/.eslintrc.js +++ b/packages/elastic-eslint-config-kibana/.eslintrc.js @@ -94,7 +94,7 @@ module.exports = { ].map(from => ({ from, to: false, - disallowedMessage: `Use "@kbn/ui-shared-deps-src/theme" to access theme vars.` + disallowedMessage: `Use "@kbn/ui-theme" to access theme vars.` })), ], ], diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/API.md b/packages/kbn-flot-charts/API.md similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/API.md rename to packages/kbn-flot-charts/API.md diff --git a/packages/kbn-flot-charts/BUILD.bazel b/packages/kbn-flot-charts/BUILD.bazel new file mode 100644 index 0000000000000..d819fa05c7d16 --- /dev/null +++ b/packages/kbn-flot-charts/BUILD.bazel @@ -0,0 +1,51 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "pkg_npm") + +PKG_BASE_NAME = "kbn-flot-charts" +PKG_REQUIRE_NAME = "@kbn/flot-charts" + +SOURCE_FILES = glob([ + "lib/**/*.js", + "index.js", +]) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "API.md", +] + +RUNTIME_DEPS = [ + "//packages/kbn-i18n", +] + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES + [ + ":srcs", + ], + deps = RUNTIME_DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/index.js b/packages/kbn-flot-charts/index.js similarity index 62% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/index.js rename to packages/kbn-flot-charts/index.js index 4f6468262e3b4..5f0620f6c61fb 100644 --- a/packages/kbn-ui-shared-deps-src/src/flot_charts/index.js +++ b/packages/kbn-flot-charts/index.js @@ -1,3 +1,11 @@ +/* + * 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. + */ + /* @notice * * This product includes code that is based on flot-charts, which was available @@ -26,15 +34,15 @@ * THE SOFTWARE. */ -import './jquery_flot'; -import './jquery_flot_canvas'; -import './jquery_flot_time'; -import './jquery_flot_symbol'; -import './jquery_flot_crosshair'; -import './jquery_flot_selection'; -import './jquery_flot_pie'; -import './jquery_flot_stack'; -import './jquery_flot_threshold'; -import './jquery_flot_fillbetween'; -import './jquery_flot_log'; -import './jquery_flot_axislabels'; +import './lib/jquery_flot'; +import './lib/jquery_flot_canvas'; +import './lib/jquery_flot_time'; +import './lib/jquery_flot_symbol'; +import './lib/jquery_flot_crosshair'; +import './lib/jquery_flot_selection'; +import './lib/jquery_flot_pie'; +import './lib/jquery_flot_stack'; +import './lib/jquery_flot_threshold'; +import './lib/jquery_flot_fillbetween'; +import './lib/jquery_flot_log'; +import './lib/jquery_flot_axislabels'; diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_colorhelpers.js b/packages/kbn-flot-charts/lib/jquery_colorhelpers.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_colorhelpers.js rename to packages/kbn-flot-charts/lib/jquery_colorhelpers.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot.js b/packages/kbn-flot-charts/lib/jquery_flot.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot.js rename to packages/kbn-flot-charts/lib/jquery_flot.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_axislabels.js b/packages/kbn-flot-charts/lib/jquery_flot_axislabels.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_axislabels.js rename to packages/kbn-flot-charts/lib/jquery_flot_axislabels.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_canvas.js b/packages/kbn-flot-charts/lib/jquery_flot_canvas.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_canvas.js rename to packages/kbn-flot-charts/lib/jquery_flot_canvas.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_categories.js b/packages/kbn-flot-charts/lib/jquery_flot_categories.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_categories.js rename to packages/kbn-flot-charts/lib/jquery_flot_categories.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_crosshair.js b/packages/kbn-flot-charts/lib/jquery_flot_crosshair.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_crosshair.js rename to packages/kbn-flot-charts/lib/jquery_flot_crosshair.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_errorbars.js b/packages/kbn-flot-charts/lib/jquery_flot_errorbars.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_errorbars.js rename to packages/kbn-flot-charts/lib/jquery_flot_errorbars.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_fillbetween.js b/packages/kbn-flot-charts/lib/jquery_flot_fillbetween.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_fillbetween.js rename to packages/kbn-flot-charts/lib/jquery_flot_fillbetween.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_image.js b/packages/kbn-flot-charts/lib/jquery_flot_image.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_image.js rename to packages/kbn-flot-charts/lib/jquery_flot_image.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_log.js b/packages/kbn-flot-charts/lib/jquery_flot_log.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_log.js rename to packages/kbn-flot-charts/lib/jquery_flot_log.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_navigate.js b/packages/kbn-flot-charts/lib/jquery_flot_navigate.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_navigate.js rename to packages/kbn-flot-charts/lib/jquery_flot_navigate.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_pie.js b/packages/kbn-flot-charts/lib/jquery_flot_pie.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_pie.js rename to packages/kbn-flot-charts/lib/jquery_flot_pie.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_resize.js b/packages/kbn-flot-charts/lib/jquery_flot_resize.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_resize.js rename to packages/kbn-flot-charts/lib/jquery_flot_resize.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_selection.js b/packages/kbn-flot-charts/lib/jquery_flot_selection.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_selection.js rename to packages/kbn-flot-charts/lib/jquery_flot_selection.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_stack.js b/packages/kbn-flot-charts/lib/jquery_flot_stack.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_stack.js rename to packages/kbn-flot-charts/lib/jquery_flot_stack.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_symbol.js b/packages/kbn-flot-charts/lib/jquery_flot_symbol.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_symbol.js rename to packages/kbn-flot-charts/lib/jquery_flot_symbol.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_threshold.js b/packages/kbn-flot-charts/lib/jquery_flot_threshold.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_threshold.js rename to packages/kbn-flot-charts/lib/jquery_flot_threshold.js diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_time.js b/packages/kbn-flot-charts/lib/jquery_flot_time.js similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot_time.js rename to packages/kbn-flot-charts/lib/jquery_flot_time.js diff --git a/packages/kbn-flot-charts/package.json b/packages/kbn-flot-charts/package.json new file mode 100755 index 0000000000000..8942cd6a05326 --- /dev/null +++ b/packages/kbn-flot-charts/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/flot-charts", + "version": "1.0.0", + "private": true, + "main": "./index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/kbn-optimizer/BUILD.bazel b/packages/kbn-optimizer/BUILD.bazel index 0e5e99c5d1096..9486f309bd0f3 100644 --- a/packages/kbn-optimizer/BUILD.bazel +++ b/packages/kbn-optimizer/BUILD.bazel @@ -70,7 +70,7 @@ TYPES_DEPS = [ "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-std:npm_module_types", "//packages/kbn-ui-shared-deps-npm:npm_module_types", - "//packages/kbn-ui-shared-deps-src", + "//packages/kbn-ui-shared-deps-src:npm_module_types", "//packages/kbn-utils:npm_module_types", "@npm//chalk", "@npm//clean-webpack-plugin", diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 58201152a2459..9454456a35c9a 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -16,7 +16,7 @@ import webpackMerge from 'webpack-merge'; import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import CompressionPlugin from 'compression-webpack-plugin'; import UiSharedDepsNpm from '@kbn/ui-shared-deps-npm'; -import UiSharedDepsSrc from '@kbn/ui-shared-deps-src'; +import * as UiSharedDepsSrc from '@kbn/ui-shared-deps-src'; import { Bundle, BundleRefs, WorkerConfig } from '../common'; import { BundleRefsPlugin } from './bundle_refs_plugin'; diff --git a/packages/kbn-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel index 873167674dd78..fc7599b058373 100644 --- a/packages/kbn-storybook/BUILD.bazel +++ b/packages/kbn-storybook/BUILD.bazel @@ -52,7 +52,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-ui-shared-deps-npm:npm_module_types", - "//packages/kbn-ui-shared-deps-src", + "//packages/kbn-ui-shared-deps-src:npm_module_types", "//packages/kbn-utils:npm_module_types", "@npm//@elastic/eui", "@npm//@emotion/cache", diff --git a/packages/kbn-storybook/src/lib/run_storybook_cli.ts b/packages/kbn-storybook/src/lib/run_storybook_cli.ts index 93197a1f2b318..fa0df75035812 100644 --- a/packages/kbn-storybook/src/lib/run_storybook_cli.ts +++ b/packages/kbn-storybook/src/lib/run_storybook_cli.ts @@ -11,7 +11,7 @@ import { logger } from '@storybook/node-logger'; import buildStandalone from '@storybook/react/standalone'; import { Flags, run } from '@kbn/dev-utils'; import UiSharedDepsNpm from '@kbn/ui-shared-deps-npm'; -import UiSharedDepsSrc from '@kbn/ui-shared-deps-src'; +import * as UiSharedDepsSrc from '@kbn/ui-shared-deps-src'; import * as constants from './constants'; // Convert the flags to a Storybook loglevel diff --git a/packages/kbn-ui-shared-deps-src/BUILD.bazel b/packages/kbn-ui-shared-deps-src/BUILD.bazel index ce2cbe714a16c..3617956b15c4a 100644 --- a/packages/kbn-ui-shared-deps-src/BUILD.bazel +++ b/packages/kbn-ui-shared-deps-src/BUILD.bazel @@ -1,10 +1,11 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") load("@npm//webpack-cli:index.bzl", webpack = "webpack_cli") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") PKG_BASE_NAME = "kbn-ui-shared-deps-src" PKG_REQUIRE_NAME = "@kbn/ui-shared-deps-src" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__ui-shared-deps-src" SOURCE_FILES = glob( [ @@ -23,8 +24,6 @@ filegroup( ) NPM_MODULE_EXTRA_FILES = [ - "flot_charts/package.json", - "theme/package.json", "package.json", "README.md" ] @@ -34,11 +33,13 @@ RUNTIME_DEPS = [ "//packages/elastic-safer-lodash-set", "//packages/kbn-analytics", "//packages/kbn-babel-preset", + "//packages/kbn-flot-charts", "//packages/kbn-i18n", "//packages/kbn-i18n-react", "//packages/kbn-monaco", "//packages/kbn-std", "//packages/kbn-ui-shared-deps-npm", + "//packages/kbn-ui-theme", ] TYPES_DEPS = [ @@ -50,7 +51,7 @@ TYPES_DEPS = [ "//packages/kbn-monaco:npm_module_types", "//packages/kbn-std:npm_module_types", "//packages/kbn-ui-shared-deps-npm:npm_module_types", - "@npm//@elastic/eui", + "//packages/kbn-ui-theme:npm_module_types", "@npm//webpack", ] @@ -105,7 +106,7 @@ webpack( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":tsc_types", ":shared_built_assets"], + deps = RUNTIME_DEPS + [":target_node", ":shared_built_assets"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) @@ -124,3 +125,20 @@ filegroup( ], visibility = ["//visibility:public"], ) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-ui-shared-deps-src/flot_charts/package.json b/packages/kbn-ui-shared-deps-src/flot_charts/package.json deleted file mode 100644 index 6c2f62447daf5..0000000000000 --- a/packages/kbn-ui-shared-deps-src/flot_charts/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/flot_charts/index.js", - "types": "../target_types/flot_charts/index.d.ts" -} \ No newline at end of file diff --git a/packages/kbn-ui-shared-deps-src/package.json b/packages/kbn-ui-shared-deps-src/package.json index 246c19496a5c0..e45e8d5496988 100644 --- a/packages/kbn-ui-shared-deps-src/package.json +++ b/packages/kbn-ui-shared-deps-src/package.json @@ -3,6 +3,5 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "target_node/index.js", - "types": "target_types/index.d.ts" + "main": "target_node/index.js" } \ No newline at end of file diff --git a/packages/kbn-ui-shared-deps-src/src/index.js b/packages/kbn-ui-shared-deps-src/src/definitions.js similarity index 89% rename from packages/kbn-ui-shared-deps-src/src/index.js rename to packages/kbn-ui-shared-deps-src/src/definitions.js index 41ea7b03c9eb3..ce859ca4b977f 100644 --- a/packages/kbn-ui-shared-deps-src/src/index.js +++ b/packages/kbn-ui-shared-deps-src/src/definitions.js @@ -8,28 +8,30 @@ const Path = require('path'); +// extracted const vars /** * Absolute path to the distributable directory */ -exports.distDir = Path.resolve(__dirname, '../shared_built_assets'); +const distDir = Path.resolve(__dirname, '../shared_built_assets'); /** * Filename of the main bundle file in the distributable directory */ -exports.jsFilename = 'kbn-ui-shared-deps-src.js'; +const jsFilename = 'kbn-ui-shared-deps-src.js'; /** * Filename of the main bundle file in the distributable directory */ -exports.cssDistFilename = 'kbn-ui-shared-deps-src.css'; +const cssDistFilename = 'kbn-ui-shared-deps-src.css'; /** * Externals mapping inteded to be used in a webpack config */ -exports.externals = { +const externals = { /** * stateful deps */ + '@kbn/ui-theme': '__kbnSharedDeps__.KbnUiTheme', '@kbn/i18n': '__kbnSharedDeps__.KbnI18n', '@kbn/i18n-react': '__kbnSharedDeps__.KbnI18nReact', '@emotion/react': '__kbnSharedDeps__.EmotionReact', @@ -43,7 +45,6 @@ exports.externals = { 'react-router-dom': '__kbnSharedDeps__.ReactRouterDom', 'styled-components': '__kbnSharedDeps__.StyledComponents', '@kbn/monaco': '__kbnSharedDeps__.KbnMonaco', - '@kbn/ui-shared-deps-src/theme': '__kbnSharedDeps__.Theme', // this is how plugins/consumers from npm load monaco 'monaco-editor/esm/vs/editor/editor.api': '__kbnSharedDeps__.MonacoBarePluginApi', @@ -77,3 +78,5 @@ exports.externals = { history: '__kbnSharedDeps__.History', classnames: '__kbnSharedDeps__.Classnames', }; + +module.exports = { distDir, jsFilename, cssDistFilename, externals }; diff --git a/packages/kbn-ui-shared-deps-src/src/entry.js b/packages/kbn-ui-shared-deps-src/src/entry.js index 424eb4b55cc64..a52dcc1440efb 100644 --- a/packages/kbn-ui-shared-deps-src/src/entry.js +++ b/packages/kbn-ui-shared-deps-src/src/entry.js @@ -11,9 +11,10 @@ require('./polyfills'); export const Jquery = require('jquery'); window.$ = window.jQuery = Jquery; // mutates window.jQuery and window.$ -require('./flot_charts'); +require('@kbn/flot-charts'); // stateful deps +export const KbnUiTheme = require('@kbn/ui-theme'); export const KbnI18n = require('@kbn/i18n'); export const KbnI18nReact = require('@kbn/i18n-react'); export const EmotionReact = require('@emotion/react'); @@ -43,7 +44,6 @@ export const ElasticEuiChartsTheme = require('@elastic/eui/dist/eui_charts_theme export const ElasticDatemath = require('@elastic/datemath'); export const ReactBeautifulDnD = require('react-beautiful-dnd'); -export const Theme = require('./theme.ts'); export const Lodash = require('lodash'); export const LodashFp = require('lodash/fp'); diff --git a/packages/kbn-ui-shared-deps-src/src/index.ts b/packages/kbn-ui-shared-deps-src/src/index.ts new file mode 100644 index 0000000000000..c5ed025969fad --- /dev/null +++ b/packages/kbn-ui-shared-deps-src/src/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { distDir, jsFilename, cssDistFilename, externals } from './definitions'; + +/** + * Absolute path to the distributable directory + */ +export { distDir }; + +/** + * Filename of the main bundle file in the distributable directory + */ +export { jsFilename }; + +/** + * Filename of the main bundle file in the distributable directory + */ +export { cssDistFilename }; + +/** + * Externals mapping inteded to be used in a webpack config + */ +export { externals }; diff --git a/packages/kbn-ui-shared-deps-src/theme/package.json b/packages/kbn-ui-shared-deps-src/theme/package.json deleted file mode 100644 index 37d60f83b18e9..0000000000000 --- a/packages/kbn-ui-shared-deps-src/theme/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/theme.js", - "types": "../target_types/theme.d.ts" -} \ No newline at end of file diff --git a/packages/kbn-ui-shared-deps-src/webpack.config.js b/packages/kbn-ui-shared-deps-src/webpack.config.js index ad84c9444fd0f..cd9f3fed1953e 100644 --- a/packages/kbn-ui-shared-deps-src/webpack.config.js +++ b/packages/kbn-ui-shared-deps-src/webpack.config.js @@ -12,7 +12,7 @@ const webpack = require('webpack'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const UiSharedDepsNpm = require('@kbn/ui-shared-deps-npm'); -const UiSharedDepsSrc = require('./src'); +const { distDir: UiSharedDepsSrcDistDir } = require('./src/definitions'); const MOMENT_SRC = require.resolve('moment/min/moment-with-locales.js'); @@ -33,7 +33,7 @@ module.exports = { context: __dirname, devtool: 'cheap-source-map', output: { - path: UiSharedDepsSrc.distDir, + path: UiSharedDepsSrcDistDir, filename: '[name].js', chunkFilename: 'kbn-ui-shared-deps-src.chunk.[id].js', sourceMapFilename: '[file].map', @@ -56,17 +56,6 @@ module.exports = { }, ], }, - { - include: [require.resolve('./src/theme.ts')], - use: [ - { - loader: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, - }, - ], - }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'], diff --git a/packages/kbn-ui-theme/BUILD.bazel b/packages/kbn-ui-theme/BUILD.bazel new file mode 100644 index 0000000000000..8e388efe23757 --- /dev/null +++ b/packages/kbn-ui-theme/BUILD.bazel @@ -0,0 +1,110 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_BASE_NAME = "kbn-ui-theme" +PKG_REQUIRE_NAME = "@kbn/ui-theme" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__ui-theme" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "@npm//@elastic/eui", +] + +TYPES_DEPS = [ + "@npm//@elastic/eui", + "@npm//@types/node", + "@npm//tslib", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-ui-theme/package.json b/packages/kbn-ui-theme/package.json new file mode 100644 index 0000000000000..40fd88b77e7b5 --- /dev/null +++ b/packages/kbn-ui-theme/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/ui-theme", + "version": "1.0.0", + "private": true, + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/kbn-ui-theme/src/index.ts b/packages/kbn-ui-theme/src/index.ts new file mode 100644 index 0000000000000..8acacd8005c9d --- /dev/null +++ b/packages/kbn-ui-theme/src/index.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export type { Theme } from './theme'; + +export { darkMode, euiDarkVars, euiLightVars, euiThemeVars, tag, version } from './theme'; diff --git a/packages/kbn-ui-shared-deps-src/src/theme.ts b/packages/kbn-ui-theme/src/theme.ts similarity index 100% rename from packages/kbn-ui-shared-deps-src/src/theme.ts rename to packages/kbn-ui-theme/src/theme.ts diff --git a/packages/kbn-ui-theme/tsconfig.json b/packages/kbn-ui-theme/tsconfig.json new file mode 100644 index 0000000000000..e1c27e88f1c91 --- /dev/null +++ b/packages/kbn-ui-theme/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "./target_types", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-ui-theme/src", + "stripInternal": true, + "types": ["node"] + }, + "include": ["src/**/*"] +} diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts index 4fdf6da00e7ab..d27add39687f9 100644 --- a/src/core/server/core_app/bundle_routes/register_bundle_routes.ts +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts @@ -10,7 +10,7 @@ import { join } from 'path'; import { PackageInfo } from '@kbn/config'; import { fromRoot } from '@kbn/utils'; import UiSharedDepsNpm from '@kbn/ui-shared-deps-npm'; -import UiSharedDepsSrc from '@kbn/ui-shared-deps-src'; +import * as UiSharedDepsSrc from '@kbn/ui-shared-deps-src'; import { IRouter } from '../../http'; import { UiPlugins } from '../../plugins'; import { FileHashCache } from './file_hash_cache'; diff --git a/src/core/server/rendering/bootstrap/get_js_dependency_paths.ts b/src/core/server/rendering/bootstrap/get_js_dependency_paths.ts index 54a901964c3fb..b04c407e5a516 100644 --- a/src/core/server/rendering/bootstrap/get_js_dependency_paths.ts +++ b/src/core/server/rendering/bootstrap/get_js_dependency_paths.ts @@ -7,7 +7,7 @@ */ import UiSharedDepsNpm from '@kbn/ui-shared-deps-npm'; -import UiSharedDepsSrc from '@kbn/ui-shared-deps-src'; +import * as UiSharedDepsSrc from '@kbn/ui-shared-deps-src'; import type { PluginInfo } from './get_plugin_bundle_paths'; export const getJsDependencyPaths = ( diff --git a/src/core/server/rendering/render_utils.ts b/src/core/server/rendering/render_utils.ts index 65cedda7ad489..b24f5968e4ee9 100644 --- a/src/core/server/rendering/render_utils.ts +++ b/src/core/server/rendering/render_utils.ts @@ -7,7 +7,7 @@ */ import UiSharedDepsNpm from '@kbn/ui-shared-deps-npm'; -import UiSharedDepsSrc from '@kbn/ui-shared-deps-src'; +import * as UiSharedDepsSrc from '@kbn/ui-shared-deps-src'; import { PublicUiSettingsParams, UserProvidedValues } from '../ui_settings'; export const getSettingValue = ( diff --git a/src/plugins/charts/public/static/components/current_time.tsx b/src/plugins/charts/public/static/components/current_time.tsx index ad05f451b607f..bac7eb241eec0 100644 --- a/src/plugins/charts/public/static/components/current_time.tsx +++ b/src/plugins/charts/public/static/components/current_time.tsx @@ -10,10 +10,7 @@ import moment, { Moment } from 'moment'; import React, { FC } from 'react'; import { LineAnnotation, AnnotationDomainType, LineAnnotationStyle } from '@elastic/charts'; -import { - euiLightVars as lightEuiTheme, - euiDarkVars as darkEuiTheme, -} from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as lightEuiTheme, euiDarkVars as darkEuiTheme } from '@kbn/ui-theme'; interface CurrentTimeProps { isDarkMode: boolean; diff --git a/src/plugins/charts/public/static/components/endzones.tsx b/src/plugins/charts/public/static/components/endzones.tsx index 695b51c9702d2..727004993d171 100644 --- a/src/plugins/charts/public/static/components/endzones.tsx +++ b/src/plugins/charts/public/static/components/endzones.tsx @@ -17,10 +17,7 @@ import { } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; -import { - euiLightVars as lightEuiTheme, - euiDarkVars as darkEuiTheme, -} from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as lightEuiTheme, euiDarkVars as darkEuiTheme } from '@kbn/ui-theme'; interface EndzonesProps { isDarkMode: boolean; diff --git a/src/plugins/data_view_editor/public/components/form_fields/type_field.tsx b/src/plugins/data_view_editor/public/components/form_fields/type_field.tsx index f6545d5fcb08e..11b46c7ee31fa 100644 --- a/src/plugins/data_view_editor/public/components/form_fields/type_field.tsx +++ b/src/plugins/data_view_editor/public/components/form_fields/type_field.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { FormattedMessage } from '@kbn/i18n-react'; import { diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index dc72cfda790d4..a3ec8fc0a9af2 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -13,7 +13,7 @@ import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom' import { EuiTab, EuiTabs, EuiToolTip, EuiBetaBadge } from '@elastic/eui'; import { I18nProvider } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; import { ApplicationStart, ChromeStart, ScopedHistory, CoreTheme } from 'src/core/public'; import { KibanaThemeProvider } from '../../kibana_react/public'; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.tsx index 6fb614327d2af..a8881b37301e0 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.tsx @@ -17,10 +17,7 @@ import { EuiDataGridCellValueElementProps, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - euiLightVars as themeLight, - euiDarkVars as themeDark, -} from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as themeLight, euiDarkVars as themeDark } from '@kbn/ui-theme'; import { DiscoverGridContext } from './discover_grid_context'; import { ElasticSearchHit } from '../../types'; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx index 372fe03666d03..6765a8d24f91a 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx @@ -8,10 +8,7 @@ import React, { useContext, useEffect } from 'react'; import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@elastic/eui'; -import { - euiLightVars as themeLight, - euiDarkVars as themeDark, -} from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as themeLight, euiDarkVars as themeDark } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import { DiscoverGridContext } from './discover_grid_context'; import { EsHitRecord } from '../../application/types'; diff --git a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx index e56c3ef2b699b..436281b119bff 100644 --- a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx @@ -7,10 +7,7 @@ */ import React, { Fragment, useContext, useEffect } from 'react'; -import { - euiLightVars as themeLight, - euiDarkVars as themeDark, -} from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as themeLight, euiDarkVars as themeDark } from '@kbn/ui-theme'; import type { DataView } from 'src/plugins/data/common'; import { diff --git a/src/plugins/discover/public/components/doc_table/components/pager/tool_bar_pagination.tsx b/src/plugins/discover/public/components/doc_table/components/pager/tool_bar_pagination.tsx index 1b07eb89b3e23..8c2e1892d8fc7 100644 --- a/src/plugins/discover/public/components/doc_table/components/pager/tool_bar_pagination.tsx +++ b/src/plugins/discover/public/components/doc_table/components/pager/tool_bar_pagination.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; interface ToolBarPaginationProps { pageSize: number; diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index abb6b01e77feb..897d69f356035 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -12,7 +12,7 @@ import { Observable, Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect'; import { EuiLoadingChart, EuiProgress } from '@elastic/eui'; -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; import { IExpressionLoaderParams, ExpressionRenderError, ExpressionRendererEvent } from './types'; import { ExpressionAstExpression, IInterpreterRenderHandlers } from '../common'; import { ExpressionLoader } from './loader'; diff --git a/src/plugins/home/public/application/components/tutorial/instruction_set.js b/src/plugins/home/public/application/components/tutorial/instruction_set.js index b2cdbde83a2c3..651212f062c8f 100644 --- a/src/plugins/home/public/application/components/tutorial/instruction_set.js +++ b/src/plugins/home/public/application/components/tutorial/instruction_set.js @@ -27,7 +27,7 @@ import { import * as StatusCheckStates from './status_check_states'; import { injectI18n, FormattedMessage } from '@kbn/i18n-react'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; class InstructionSetUi extends React.Component { constructor(props) { diff --git a/src/plugins/interactive_setup/public/cluster_configuration_form.tsx b/src/plugins/interactive_setup/public/cluster_configuration_form.tsx index 1282f6612dd23..5ce4068039b85 100644 --- a/src/plugins/interactive_setup/public/cluster_configuration_form.tsx +++ b/src/plugins/interactive_setup/public/cluster_configuration_form.tsx @@ -38,7 +38,7 @@ import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; import type { Certificate } from '../common'; import { DocLink } from './doc_link'; diff --git a/src/plugins/interactive_setup/public/single_chars_field.tsx b/src/plugins/interactive_setup/public/single_chars_field.tsx index bd66b5ac1fcf1..1a6a513583bcc 100644 --- a/src/plugins/interactive_setup/public/single_chars_field.tsx +++ b/src/plugins/interactive_setup/public/single_chars_field.tsx @@ -13,7 +13,7 @@ import useList from 'react-use/lib/useList'; import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import { i18n } from '@kbn/i18n'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; export interface SingleCharsFieldProps { defaultValue: string; diff --git a/src/plugins/interactive_setup/public/use_verification.tsx b/src/plugins/interactive_setup/public/use_verification.tsx index fed055a415ebb..42bcf94ad1f8c 100644 --- a/src/plugins/interactive_setup/public/use_verification.tsx +++ b/src/plugins/interactive_setup/public/use_verification.tsx @@ -11,7 +11,7 @@ import constate from 'constate'; import type { FunctionComponent } from 'react'; import React, { useEffect, useRef, useState } from 'react'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; import { useKibana } from './use_kibana'; import { VerificationCodeForm } from './verification_code_form'; diff --git a/src/plugins/kibana_react/common/eui_styled_components.tsx b/src/plugins/kibana_react/common/eui_styled_components.tsx index 4f680d944b958..ab45c172fc1b0 100644 --- a/src/plugins/kibana_react/common/eui_styled_components.tsx +++ b/src/plugins/kibana_react/common/eui_styled_components.tsx @@ -10,7 +10,7 @@ import type { DecoratorFn } from '@storybook/react'; import React from 'react'; import * as styledComponents from 'styled-components'; import { ThemedStyledComponentsModule, ThemeProvider, ThemeProviderProps } from 'styled-components'; -import { euiThemeVars, euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars, euiLightVars, euiDarkVars } from '@kbn/ui-theme'; export interface EuiTheme { eui: typeof euiThemeVars; diff --git a/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx b/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx index 85263b7006c16..4fe4b87d6d106 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx @@ -7,10 +7,7 @@ */ import React from 'react'; -import { - euiLightVars as lightTheme, - euiDarkVars as darkTheme, -} from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as lightTheme, euiDarkVars as darkTheme } from '@kbn/ui-theme'; import { EuiFormControlLayout } from '@elastic/eui'; import { CodeEditor, Props } from './code_editor'; diff --git a/src/plugins/kibana_react/public/code_editor/editor_theme.ts b/src/plugins/kibana_react/public/code_editor/editor_theme.ts index 517fe4cf61a08..9242b7319e5c9 100644 --- a/src/plugins/kibana_react/public/code_editor/editor_theme.ts +++ b/src/plugins/kibana_react/public/code_editor/editor_theme.ts @@ -8,10 +8,7 @@ import { monaco } from '@kbn/monaco'; -import { - euiLightVars as lightTheme, - euiDarkVars as darkTheme, -} from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as lightTheme, euiDarkVars as darkTheme } from '@kbn/ui-theme'; // NOTE: For talk around where this theme information will ultimately live, // please see this discuss issue: https://github.com/elastic/kibana/issues/43814 diff --git a/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js b/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js index 13c17b8f4c38f..bdab3b45a00f6 100644 --- a/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js @@ -7,7 +7,7 @@ */ import { cloneDeep } from 'lodash'; import 'jest-canvas-mock'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; import { VegaParser } from './vega_parser'; import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; diff --git a/src/plugins/vis_types/vega/public/data_model/vega_parser.ts b/src/plugins/vis_types/vega/public/data_model/vega_parser.ts index 58daf67e31c9d..bcaf8afd4fd0c 100644 --- a/src/plugins/vis_types/vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_types/vega/public/data_model/vega_parser.ts @@ -11,7 +11,7 @@ import schemaParser from 'vega-schema-url-parser'; import versionCompare from 'compare-versions'; import hjson from 'hjson'; import { euiPaletteColorBlind } from '@elastic/eui'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import { logger, Warn, None, version as vegaVersion } from 'vega'; diff --git a/src/plugins/vis_types/vislib/public/vislib/components/tooltip/tooltip.js b/src/plugins/vis_types/vislib/public/vislib/components/tooltip/tooltip.js index 1faebdf0ce89c..2d6e1728668fd 100644 --- a/src/plugins/vis_types/vislib/public/vislib/components/tooltip/tooltip.js +++ b/src/plugins/vis_types/vislib/public/vislib/components/tooltip/tooltip.js @@ -12,7 +12,7 @@ import $ from 'jquery'; import { Binder } from '../../lib/binder'; import { positionTooltip } from './position_tooltip'; -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; let allContents = []; diff --git a/x-pack/plugins/apm/common/viz_colors.ts b/x-pack/plugins/apm/common/viz_colors.ts index 5b4946f346841..20d525c914549 100644 --- a/x-pack/plugins/apm/common/viz_colors.ts +++ b/x-pack/plugins/apm/common/viz_colors.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { euiLightVars as lightTheme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as lightTheme } from '@kbn/ui-theme'; function getVizColorsForTheme(theme = lightTheme) { return [ diff --git a/x-pack/plugins/apm/public/application/uxApp.tsx b/x-pack/plugins/apm/public/application/uxApp.tsx index dde7cfe5399d3..fa29f04fccbad 100644 --- a/x-pack/plugins/apm/public/application/uxApp.tsx +++ b/x-pack/plugins/apm/public/application/uxApp.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-theme'; import { EuiErrorBoundary } from '@elastic/eui'; import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; diff --git a/x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/percentile_annotations.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/percentile_annotations.tsx index 74d9ccdcda8d8..9982f8a85583c 100644 --- a/x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/percentile_annotations.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/page_load_distribution/percentile_annotations.tsx @@ -13,7 +13,7 @@ import { LineAnnotationStyle, Position, } from '@elastic/charts'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { EuiToolTip } from '@elastic/eui'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/url_search/render_option.tsx b/x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/url_search/render_option.tsx index 3293760ef7128..9bc9be79bfb56 100644 --- a/x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/url_search/render_option.tsx +++ b/x-pack/plugins/apm/public/components/app/rum_dashboard/url_filter/url_search/render_option.tsx @@ -8,7 +8,7 @@ import React, { ReactNode } from 'react'; import { EuiHighlight, EuiSelectableOption } from '@elastic/eui'; import styled from 'styled-components'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; const StyledSpan = styled.span` color: ${euiLightVars.euiColorSuccessText}; diff --git a/x-pack/plugins/apm/public/components/app/service_map/Controls.test.tsx b/x-pack/plugins/apm/public/components/app/service_map/Controls.test.tsx index f2dd9cce8f27e..0be6bf3e3f545 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Controls.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Controls.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { euiLightVars as lightTheme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as lightTheme } from '@kbn/ui-theme'; import { render } from '@testing-library/react'; import cytoscape from 'cytoscape'; import React, { ReactNode } from 'react'; diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index c7b98743c6bea..5a6749b08e5b7 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-theme'; import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config'; import React from 'react'; import { Route } from 'react-router-dom'; diff --git a/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts b/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts index 1b44a90fe7bfc..4caa3bd7701f0 100644 --- a/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts +++ b/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; const { euiColorDarkShade, euiColorWarning } = theme; export const errorColor = '#c23c2b'; diff --git a/x-pack/plugins/apm/server/routes/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/routes/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 790fcf5720745..1604d22a209a7 100644 --- a/x-pack/plugins/apm/server/routes/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/routes/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -6,7 +6,7 @@ */ import { sum, round } from 'lodash'; -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; import { isFiniteNumber } from '../../../../../../common/utils/is_finite_number'; import { Setup } from '../../../../../lib/helpers/setup_request'; import { getMetricsDateHistogramParams } from '../../../../../lib/helpers/metrics'; diff --git a/x-pack/plugins/apm/server/routes/metrics/by_agent/java/gc/get_gc_rate_chart.ts b/x-pack/plugins/apm/server/routes/metrics/by_agent/java/gc/get_gc_rate_chart.ts index a22f7df915617..0476025594b26 100644 --- a/x-pack/plugins/apm/server/routes/metrics/by_agent/java/gc/get_gc_rate_chart.ts +++ b/x-pack/plugins/apm/server/routes/metrics/by_agent/java/gc/get_gc_rate_chart.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_GC_COUNT } from '../../../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../../../../lib/helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/routes/metrics/by_agent/java/gc/get_gc_time_chart.ts b/x-pack/plugins/apm/server/routes/metrics/by_agent/java/gc/get_gc_time_chart.ts index 596d871b916f9..b1ef7b5e106f5 100644 --- a/x-pack/plugins/apm/server/routes/metrics/by_agent/java/gc/get_gc_time_chart.ts +++ b/x-pack/plugins/apm/server/routes/metrics/by_agent/java/gc/get_gc_time_chart.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_GC_TIME } from '../../../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../../../../lib/helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/routes/metrics/by_agent/java/heap_memory/index.ts b/x-pack/plugins/apm/server/routes/metrics/by_agent/java/heap_memory/index.ts index cde28e77e38ca..d57dfb184ca88 100644 --- a/x-pack/plugins/apm/server/routes/metrics/by_agent/java/heap_memory/index.ts +++ b/x-pack/plugins/apm/server/routes/metrics/by_agent/java/heap_memory/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_HEAP_MEMORY_MAX, diff --git a/x-pack/plugins/apm/server/routes/metrics/by_agent/java/non_heap_memory/index.ts b/x-pack/plugins/apm/server/routes/metrics/by_agent/java/non_heap_memory/index.ts index ffcce74ee6766..379962d928e28 100644 --- a/x-pack/plugins/apm/server/routes/metrics/by_agent/java/non_heap_memory/index.ts +++ b/x-pack/plugins/apm/server/routes/metrics/by_agent/java/non_heap_memory/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_NON_HEAP_MEMORY_MAX, diff --git a/x-pack/plugins/apm/server/routes/metrics/by_agent/java/thread_count/index.ts b/x-pack/plugins/apm/server/routes/metrics/by_agent/java/thread_count/index.ts index 699812ffc8463..b9a49acb3d16e 100644 --- a/x-pack/plugins/apm/server/routes/metrics/by_agent/java/thread_count/index.ts +++ b/x-pack/plugins/apm/server/routes/metrics/by_agent/java/thread_count/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_THREAD_COUNT, diff --git a/x-pack/plugins/apm/server/routes/metrics/by_agent/shared/cpu/index.ts b/x-pack/plugins/apm/server/routes/metrics/by_agent/shared/cpu/index.ts index 95c39d4bd55cc..d09b35e25e396 100644 --- a/x-pack/plugins/apm/server/routes/metrics/by_agent/shared/cpu/index.ts +++ b/x-pack/plugins/apm/server/routes/metrics/by_agent/shared/cpu/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import { METRIC_SYSTEM_CPU_PERCENT, diff --git a/x-pack/plugins/apm/server/routes/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/routes/metrics/fetch_and_transform_metrics.ts index ea06b35c1fc45..52a099bdcf405 100644 --- a/x-pack/plugins/apm/server/routes/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/routes/metrics/fetch_and_transform_metrics.ts @@ -6,7 +6,7 @@ */ import { Unionize } from 'utility-types'; -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { getVizColorForIndex } from '../../../common/viz_colors'; import { AggregationOptionsByType } from '../../../../../../src/core/types/elasticsearch'; diff --git a/x-pack/plugins/canvas/shareable_runtime/api/index.ts b/x-pack/plugins/canvas/shareable_runtime/api/index.ts index 24b0632e2d74c..ad9e4fcf03cdc 100644 --- a/x-pack/plugins/canvas/shareable_runtime/api/index.ts +++ b/x-pack/plugins/canvas/shareable_runtime/api/index.ts @@ -9,6 +9,6 @@ import 'core-js/stable'; import 'regenerator-runtime/runtime'; import 'whatwg-fetch'; import 'jquery'; -import '@kbn/ui-shared-deps-src/flot_charts'; +import '@kbn/flot-charts'; export * from './shareable'; diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 477738ddeac16..5f0c87168375d 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiDarkVars } from '@kbn/ui-theme'; import { I18nProvider } from '@kbn/i18n-react'; import { ThemeProvider } from 'styled-components'; diff --git a/x-pack/plugins/cases/public/components/header_page/index.test.tsx b/x-pack/plugins/cases/public/components/header_page/index.test.tsx index 0c0ff060fb2bc..4f0da554f3d1b 100644 --- a/x-pack/plugins/cases/public/components/header_page/index.test.tsx +++ b/x-pack/plugins/cases/public/components/header_page/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiDarkVars } from '@kbn/ui-theme'; import { shallow } from 'enzyme'; import React from 'react'; diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx index 43ebd9bee3ca9..a1930c17a70b1 100644 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx +++ b/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiDarkVars } from '@kbn/ui-theme'; import { mount, shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../common/mock'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_color_range.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_color_range.ts index 92a88f4d60670..2c49f92e68d8e 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_color_range.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_color_range.ts @@ -7,10 +7,7 @@ import d3 from 'd3'; import { useMemo } from 'react'; -import { - euiLightVars as euiThemeLight, - euiDarkVars as euiThemeDark, -} from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as euiThemeLight, euiDarkVars as euiThemeDark } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx index 8d04a6f3cefc3..b0b9c471b7cb4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; -import { euiLightVars as euiVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as euiVars } from '@kbn/ui-theme'; import type { Agent } from '../../../types'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx index d98e9d8673755..8d5405d3fb469 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx @@ -7,7 +7,7 @@ import { euiPaletteColorBlindBehindText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import type { SimplifiedAgentStatus } from '../../../types'; diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts index 7cd89b2464c4e..010f6e99e39bc 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts @@ -7,7 +7,7 @@ import chroma from 'chroma-js'; import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; -import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-theme'; import { isColorDark } from '@elastic/eui'; import type { Datatable } from 'src/plugins/expressions/public'; import { diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts index be7f6f1d1d225..cb80271f6842d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts @@ -8,7 +8,7 @@ import { uniq, mapValues } from 'lodash'; import type { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; import type { Datatable } from 'src/plugins/expressions'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import type { AccessorConfig, FramePublicAPI } from '../types'; import { getColumnToLabelMap } from './state_helpers'; import { FormatFactory, LayerType, layerTypes } from '../../common'; diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx index d41baff0bc1dc..7408987261b41 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx @@ -12,7 +12,7 @@ import { EuiIcon } from '@elastic/eui'; import { RectAnnotation, AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts'; import type { PaletteRegistry } from 'src/plugins/charts/public'; import type { FieldFormat } from 'src/plugins/field_formats/common'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import type { LayerArgs, YConfig } from '../../common/expressions'; import type { LensMultiTable } from '../../common/types'; import { hasIcon } from './xy_config_panel/reference_line_panel'; diff --git a/x-pack/plugins/maps/public/reducers/map/default_map_settings.ts b/x-pack/plugins/maps/public/reducers/map/default_map_settings.ts index ebde5481a13f5..f5af113b3b316 100644 --- a/x-pack/plugins/maps/public/reducers/map/default_map_settings.ts +++ b/x-pack/plugins/maps/public/reducers/map/default_map_settings.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; import { INITIAL_LOCATION, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; import { MapSettings } from './types'; diff --git a/x-pack/plugins/ml/common/util/group_color_utils.ts b/x-pack/plugins/ml/common/util/group_color_utils.ts index 63f0e13676d58..b9709671475be 100644 --- a/x-pack/plugins/ml/common/util/group_color_utils.ts +++ b/x-pack/plugins/ml/common/util/group_color_utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { euiDarkVars as euiVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiDarkVars as euiVars } from '@kbn/ui-theme'; import { stringHash } from './string_utils'; diff --git a/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts b/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts index 2ccc687d145d0..c1ed6bff306f9 100644 --- a/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts +++ b/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts @@ -7,10 +7,7 @@ import d3 from 'd3'; import { useMemo } from 'react'; -import { - euiLightVars as euiThemeLight, - euiDarkVars as euiThemeDark, -} from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as euiThemeLight, euiDarkVars as euiThemeDark } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx index 4b9f9d86799df..b385ec2bcf6ec 100644 --- a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx +++ b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -17,7 +17,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; import { JobMessage } from '../../../../common/types/audit_message'; import { JobIcon } from '../job_message_icon'; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx index c9851589f4c46..c1857979e7d53 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx @@ -10,7 +10,7 @@ import { render, waitFor, screen } from '@testing-library/react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { euiLightVars as euiThemeLight } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as euiThemeLight } from '@kbn/ui-theme'; import { ScatterplotMatrix } from './scatterplot_matrix'; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts index ed8a49cd36f02..5bc2205827c97 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts @@ -10,7 +10,7 @@ import 'jest-canvas-mock'; // @ts-ignore import { compile } from 'vega-lite/build/vega-lite'; -import { euiLightVars as euiThemeLight } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as euiThemeLight } from '@kbn/ui-theme'; import { LEGEND_TYPES } from '../vega_chart/common'; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts index 83525a4837dc9..442695ea9c811 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts @@ -9,7 +9,7 @@ // @ts-ignore import type { TopLevelSpec } from 'vega-lite/build/vega-lite'; -import { euiLightVars as euiThemeLight } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as euiThemeLight } from '@kbn/ui-theme'; import { euiPaletteColorBlind, euiPaletteNegative, euiPalettePositive } from '@elastic/eui'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx index 2d116e0dd851e..3ca1f65cf2ecc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx @@ -10,7 +10,7 @@ import type { TopLevelSpec } from 'vega-lite/build/vega-lite'; import { euiPaletteColorBlind, euiPaletteGray } from '@elastic/eui'; -import { euiLightVars as euiThemeLight } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as euiThemeLight } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx index d91b742b8cfe1..dfb95887b2e99 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx @@ -24,7 +24,7 @@ import { EuiIcon } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { euiLightVars as euiVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as euiVars } from '@kbn/ui-theme'; import type { DecisionPathPlotData } from './use_classification_path_data'; import { formatSingleValue } from '../../../../../formatters/format_value'; import { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx index 534459dd074f0..8d5d4c5e4ca23 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx @@ -21,7 +21,7 @@ import { BarSeriesSpec, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { euiLightVars as euiVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as euiVars } from '@kbn/ui-theme'; import { TotalFeatureImportance, isClassificationTotalFeatureImportance, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts index 3d386073849f4..e51fb68ce7096 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts @@ -5,10 +5,7 @@ * 2.0. */ -import { - euiLightVars as lightTheme, - euiDarkVars as darkTheme, -} from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as lightTheme, euiDarkVars as darkTheme } from '@kbn/ui-theme'; import { JobCreatorType, isMultiMetricJobCreator, diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/palette_legends.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/palette_legends.tsx index b9686cc2eccc1..98305dbfdd840 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/palette_legends.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/palette_legends.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n-react'; -import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-theme'; import { getCoreVitalTooltipMessage, Thresholds } from './core_vital_item'; import { useUiSetting$ } from '../../../../../../../src/plugins/kibana_react/public'; import { diff --git a/x-pack/plugins/osquery/public/application.tsx b/x-pack/plugins/osquery/public/application.tsx index 17d47f757a2a6..754d924529b19 100644 --- a/x-pack/plugins/osquery/public/application.tsx +++ b/x-pack/plugins/osquery/public/application.tsx @@ -6,7 +6,7 @@ */ import { EuiErrorBoundary } from '@elastic/eui'; -import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-theme'; import React, { useMemo } from 'react'; import ReactDOM from 'react-dom'; import { Router } from 'react-router-dom'; diff --git a/x-pack/plugins/security/public/components/token_field.tsx b/x-pack/plugins/security/public/components/token_field.tsx index 38a8e45cbb5b5..7363ce7b28ff8 100644 --- a/x-pack/plugins/security/public/components/token_field.tsx +++ b/x-pack/plugins/security/public/components/token_field.tsx @@ -22,7 +22,7 @@ import type { FunctionComponent, ReactElement } from 'react'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; export interface TokenFieldProps extends Omit { value: string; diff --git a/x-pack/plugins/security/server/prompt_page.tsx b/x-pack/plugins/security/server/prompt_page.tsx index bcb2dcf810f30..b3052c6e8db63 100644 --- a/x-pack/plugins/security/server/prompt_page.tsx +++ b/x-pack/plugins/security/server/prompt_page.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; import UiSharedDepsNpm from '@kbn/ui-shared-deps-npm'; -import UiSharedDepsSrc from '@kbn/ui-shared-deps-src'; +import * as UiSharedDepsSrc from '@kbn/ui-shared-deps-src'; import type { IBasePath } from 'src/core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx index 231f93e896df9..8695ad1539477 100644 --- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx @@ -8,7 +8,7 @@ import { storiesOf } from '@storybook/react'; import React, { ReactNode } from 'react'; import { ThemeProvider } from 'styled-components'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { AndOrBadge } from '..'; diff --git a/x-pack/plugins/security_solution/public/common/components/conditions_table/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/conditions_table/index.stories.tsx index 9efbbc7a3211d..61451cf5a91d9 100644 --- a/x-pack/plugins/security_solution/public/common/components/conditions_table/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/conditions_table/index.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { storiesOf, addDecorator } from '@storybook/react'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { createItems, TEST_COLUMNS } from './test_utils'; import { ConditionsTable } from '.'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx index 898a9e3ab0388..24c22f8ef1ded 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx @@ -9,7 +9,7 @@ import { storiesOf, addDecorator } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { ExceptionItem } from './'; import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx index de56e0eefc1ac..05cbe352fa72e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx @@ -9,7 +9,7 @@ import { storiesOf, addDecorator } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionsViewerHeader } from './exceptions_viewer_header'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx index 2e25a357e86b1..5968322ac3539 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiDarkVars } from '@kbn/ui-theme'; import { shallow } from 'enzyme'; import React from 'react'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx index 07a5ad475aed2..47b6451dd3090 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiDarkVars } from '@kbn/ui-theme'; import { mount, shallow } from 'enzyme'; import React from 'react'; diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx index 74a3d1c3999e9..38f08698e5428 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { storiesOf, addDecorator } from '@storybook/react'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { ItemDetailsAction, ItemDetailsCard, ItemDetailsPropertySummary } from '.'; diff --git a/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx index 146ba8ef82505..3e0cc2d34ce1a 100644 --- a/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { storiesOf, addDecorator } from '@storybook/react'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { TextFieldValue } from '.'; diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/logic_buttons.stories.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/logic_buttons.stories.tsx index 6497875ac8d4a..32145412823ec 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/logic_buttons.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/logic_buttons.stories.tsx @@ -9,7 +9,7 @@ import { storiesOf, addDecorator } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { LogicButtons } from './logic_buttons'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar.test.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar.test.tsx index 73acaa48983b4..ac4b2d44204d1 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiDarkVars } from '@kbn/ui-theme'; import { mount, shallow } from 'enzyme'; import React from 'react'; diff --git a/x-pack/plugins/security_solution/public/common/lib/theme/use_eui_theme.tsx b/x-pack/plugins/security_solution/public/common/lib/theme/use_eui_theme.tsx index 0057666ba4262..e890d9fe6d650 100644 --- a/x-pack/plugins/security_solution/public/common/lib/theme/use_eui_theme.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/theme/use_eui_theme.tsx @@ -5,10 +5,7 @@ * 2.0. */ -import { - euiLightVars as lightTheme, - euiDarkVars as darkTheme, -} from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as lightTheme, euiDarkVars as darkTheme } from '@kbn/ui-theme'; import { DEFAULT_DARK_MODE } from '../../../../common/constants'; import { useUiSetting$ } from '../kibana'; diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 9ad5abc1c7ed2..1909388aea27a 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiDarkVars } from '@kbn/ui-theme'; import { I18nProvider } from '@kbn/i18n-react'; import React from 'react'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx index 835ab73282f1a..af746d158e2a7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx @@ -7,7 +7,7 @@ import { upperFirst } from 'lodash/fp'; import React from 'react'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; import { HealthTruncateText } from '../../../../common/components/health_truncate_text'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx index df50946f058ba..b31af0ab269ed 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx @@ -7,7 +7,7 @@ import styled from 'styled-components'; import { EuiHealth } from '@elastic/eui'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import React from 'react'; import { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.test.tsx index 1badc0206d12e..d7e099316cb12 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.test.tsx @@ -13,7 +13,7 @@ import { HostRiskScore } from './host_risk_score'; import { EuiHealth, EuiHealthProps } from '@elastic/eui'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); diff --git a/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.tsx b/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.tsx index 3f666cf396504..982cde1e90a00 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { EuiHealth, transparentize } from '@elastic/eui'; import styled, { css } from 'styled-components'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { HostRiskSeverity } from '../../../../common/search_strategy'; const HOST_RISK_SEVERITY_COLOUR = { diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx index eb34f9100101b..9b2c017d3f8d1 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx @@ -17,7 +17,7 @@ import { LineAnnotation, TooltipValue, } from '@elastic/charts'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart, EuiText, EuiPanel } from '@elastic/eui'; import styled from 'styled-components'; import { chartDefaultSettings, useTheme } from '../../../common/components/charts/common'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx index 9e7e01c64a432..27e0f49bd6de3 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { InspectButton, BUTTON_CLASS as INPECT_BUTTON_CLASS, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx index 09321244e0abc..89fe46445b20e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { addDecorator, storiesOf } from '@storybook/react'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { EuiCheckbox, EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; import { OperatingSystem } from '../../../../../../../common/endpoint/types'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.stories.tsx index ecc18d5d52fd9..79ada90287192 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { ThemeProvider } from 'styled-components'; import { storiesOf } from '@storybook/react'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { EuiHorizontalRule } from '@elastic/eui'; import { KibanaContextProvider } from '../../../../../../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.stories.tsx index 484f17318f839..8ba70769838a3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.stories.tsx @@ -8,7 +8,7 @@ import React, { useState } from 'react'; import { ThemeProvider } from 'styled-components'; import { storiesOf, addDecorator } from '@storybook/react'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-theme'; import { ViewType } from '../../../state'; import { ViewTypeToggle } from '.'; diff --git a/x-pack/plugins/security_solution/public/network/components/details/index.tsx b/x-pack/plugins/security_solution/public/network/components/details/index.tsx index 5cd2f4dfd72c8..af9b5138d853f 100644 --- a/x-pack/plugins/security_solution/public/network/components/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/details/index.tsx @@ -5,10 +5,7 @@ * 2.0. */ -import { - euiLightVars as lightTheme, - euiDarkVars as darkTheme, -} from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as lightTheme, euiDarkVars as darkTheme } from '@kbn/ui-theme'; import React from 'react'; import { DEFAULT_DARK_MODE } from '../../../../common/constants'; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx index a557ee7b8b190..fd57256602aa5 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx @@ -14,7 +14,7 @@ import { EuiIcon, EuiText, } from '@elastic/eui'; -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; import styled from 'styled-components'; import * as i18n from '../translations'; 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 65e85e99e87a0..06f7787085ddc 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 @@ -6,10 +6,7 @@ */ import { EuiFlexItem, EuiFlexGroup, EuiHorizontalRule } from '@elastic/eui'; -import { - euiLightVars as lightTheme, - euiDarkVars as darkTheme, -} from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as lightTheme, euiDarkVars as darkTheme } from '@kbn/ui-theme'; import { getOr } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts b/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts index f52075cbe4d85..182d710c3c45f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { darkMode, euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { darkMode, euiThemeVars } from '@kbn/ui-theme'; import { useMemo } from 'react'; type ResolverColorNames = diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts b/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts index f5a9c37623c47..f2fef61d6f385 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; import { ButtonColor } from '@elastic/eui'; import { useMemo } from 'react'; import { ResolverProcessType, NodeDataStatus } from '../types'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx index 73287c3cf5cec..4aba02607ec2e 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; import { mount } from 'enzyme'; import { omit, set } from 'lodash/fp'; import React from 'react'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx index 1c2ac89119abb..35fd814a7c2f4 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; import { EuiDataGridColumnActions } from '@elastic/eui'; import { keyBy } from 'lodash/fp'; import React from 'react'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/constants.ts b/x-pack/plugins/timelines/public/components/t_grid/body/constants.ts index 2367e6dc38e4c..3f1889732483d 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/constants.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/body/constants.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; /** * This is the effective width in pixels of an action button used with diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx index 56388b16b1b66..e655037732650 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx @@ -18,7 +18,7 @@ import { addBuildingBlockStyle, } from './helpers'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiThemeVars } from '@kbn/ui-theme'; import { mockDnsEvent } from '../../../mock'; describe('helpers', () => { diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx index 0dc8ff58d2ef1..d58cf96e81475 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiDarkVars } from '@kbn/ui-theme'; import React from 'react'; import { render, screen } from '@testing-library/react'; import { TGridIntegrated, TGridIntegratedProps } from './index'; diff --git a/x-pack/plugins/timelines/public/mock/test_providers.tsx b/x-pack/plugins/timelines/public/mock/test_providers.tsx index 12f8a5329af6a..179b4e49cc9a9 100644 --- a/x-pack/plugins/timelines/public/mock/test_providers.tsx +++ b/x-pack/plugins/timelines/public/mock/test_providers.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiDarkVars } from '@kbn/ui-theme'; import { I18nProvider } from '@kbn/i18n-react'; import React from 'react'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx index ce03d5989bc15..2783b204fff2d 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx @@ -10,7 +10,7 @@ import React, { useState } from 'react'; import { EuiSpacer, EuiBasicTable } from '@elastic/eui'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx index 6cf5e172ff284..b8c25eef938bb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx @@ -17,7 +17,7 @@ import { EuiSelect, EuiLoadingChart, } from '@elastic/eui'; -import { euiLightVars as lightEuiTheme } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars as lightEuiTheme } from '@kbn/ui-theme'; import { Axis, BarSeries, Chart, CurveType, LineSeries, Settings } from '@elastic/charts'; import { assign, fill } from 'lodash'; import moment from 'moment'; diff --git a/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx index 6df3879ef7407..2d25cf6e84e1b 100644 --- a/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx +++ b/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-theme'; import React, { createContext, useMemo } from 'react'; import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { DARK_THEME, LIGHT_THEME, PartialTheme, Theme } from '@elastic/charts'; diff --git a/yarn.lock b/yarn.lock index 47a01e6cb8aee..2e4d6ddc987fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2950,6 +2950,10 @@ version "0.0.0" uid "" +"@kbn/flot-charts@link:bazel-bin/packages/kbn-flot-charts": + version "0.0.0" + uid "" + "@kbn/i18n-react@link:bazel-bin/packages/kbn-i18n-react": version "0.0.0" uid "" @@ -3114,6 +3118,10 @@ version "0.0.0" uid "" +"@kbn/ui-theme@link:bazel-bin/packages/kbn-ui-theme": + version "0.0.0" + uid "" + "@kbn/utility-types@link:bazel-bin/packages/kbn-utility-types": version "0.0.0" uid "" @@ -5960,6 +5968,14 @@ version "0.0.0" uid "" +"@types/kbn__ui-shared-deps-src@link:bazel-bin/packages/kbn-ui-shared-deps-src/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__ui-theme@link:bazel-bin/packages/kbn-ui-theme/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__utility-types@link:bazel-bin/packages/kbn-utility-types/npm_module_types": version "0.0.0" uid "" From eb17b102039f358113a8f81e47ef278d90348cf4 Mon Sep 17 00:00:00 2001 From: Sandra G Date: Thu, 20 Jan 2022 17:13:23 -0500 Subject: [PATCH 30/43] [Stack Monitoring] compatibility for agent data streams (#119112) * update queries for elasticsearch package * fix unit test * add gitCcs helper function * modify rest of es queries * update logstash and kibana queries to use new createQuery * change beats and apm to use new createQuery * update changeQuery and remove old one * make getIndexPattern take request to check for ccs * fix unit test * fix unit tests * update queries and createQuery * don't add metric constant without dataset in query * fix types * fix type * comment out mb tests * fix unit test * fix unit test * fix * fix function param * change to getMetrics name * change to node_stats * comment out metricbeat tests * fix types * improve types and readability for test * remove passing of data stream type for now * add tests for createQuery changes * update getNewIndexPatterns to take one dataset * add unit test for getNewIndexPatterns * fix types * remove metrics from filter, update tests * update createNewIndexPatterns to accept new config instead of legacy * update alert queries to include datas stream index patterns * update comment * fix defaulting ccs to * for non cluster requests * update elasticsearch enterprise module * update unit test * remove data_stream.type from queries * change entsearch to metricbeat module name enterprisesearch * undo ccs cluster stats change * fix import * update alert queries * fix unit test * update unit test * change shard size query to use filter * change must to filter fix * update findSupportedBasicLicenseCluster index pattern * add ccs param to cluster request functions * update queries for ccs in get_clusters_from_request * update getBeatsForClusters query * update clusters apm query * update enterprisesearch query * move index pattern to query in fetch for alerts, fix ccs * remove metricbeat config from alert tests * fix ts * add metricset.name back to queries * comment tests back in * remove enterprise search checking for standalone cluster to fix test * update es index metricset name from index_stats to index for mb data * fix type * fetchClusters creates index pattern * fix type * remove monitoring.ui.metricbeat.index from config and usage in getCollectionStatus * fix type Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/monitoring/common/ccs_utils.ts | 59 +---- x-pack/plugins/monitoring/common/constants.ts | 12 + .../monitoring/server/alerts/base_rule.ts | 25 +- .../alerts/ccr_read_exceptions_rule.test.ts | 1 - .../server/alerts/ccr_read_exceptions_rule.ts | 16 +- .../server/alerts/cluster_health_rule.test.ts | 1 - .../server/alerts/cluster_health_rule.ts | 23 +- .../server/alerts/cpu_usage_rule.test.ts | 1 - .../server/alerts/cpu_usage_rule.ts | 12 +- .../server/alerts/disk_usage_rule.test.ts | 1 - .../server/alerts/disk_usage_rule.ts | 12 +- ...lasticsearch_version_mismatch_rule.test.ts | 1 - .../elasticsearch_version_mismatch_rule.ts | 16 +- .../kibana_version_mismatch_rule.test.ts | 1 - .../alerts/kibana_version_mismatch_rule.ts | 16 +- .../alerts/large_shard_size_rule.test.ts | 1 - .../server/alerts/large_shard_size_rule.ts | 16 +- .../alerts/license_expiration_rule.test.ts | 1 - .../server/alerts/license_expiration_rule.ts | 17 +- .../logstash_version_mismatch_rule.test.ts | 1 - .../alerts/logstash_version_mismatch_rule.ts | 16 +- .../server/alerts/memory_usage_rule.test.ts | 1 - .../server/alerts/memory_usage_rule.ts | 16 +- .../missing_monitoring_data_rule.test.ts | 1 - .../alerts/missing_monitoring_data_rule.ts | 12 +- .../server/alerts/nodes_changed_rule.test.ts | 1 - .../server/alerts/nodes_changed_rule.ts | 18 +- .../thread_pool_rejections_rule_base.ts | 12 +- ...thread_pool_search_rejections_rule.test.ts | 1 - .../thread_pool_write_rejections_rule.test.ts | 1 - .../plugins/monitoring/server/config.test.ts | 3 - x-pack/plugins/monitoring/server/config.ts | 3 - .../collectors/get_usage_collector.ts | 5 +- .../server/lib/alerts/append_mb_index.ts | 12 - .../lib/alerts/create_dataset_query_filter.ts | 24 ++ .../lib/alerts/fetch_ccr_read_exceptions.ts | 19 +- .../lib/alerts/fetch_cluster_health.test.ts | 15 +- .../server/lib/alerts/fetch_cluster_health.ts | 19 +- .../server/lib/alerts/fetch_clusters.test.ts | 21 +- .../server/lib/alerts/fetch_clusters.ts | 18 +- .../alerts/fetch_cpu_usage_node_stats.test.ts | 34 ++- .../lib/alerts/fetch_cpu_usage_node_stats.ts | 20 +- .../fetch_disk_usage_node_stats.test.ts | 15 +- .../lib/alerts/fetch_disk_usage_node_stats.ts | 19 +- .../fetch_elasticsearch_versions.test.ts | 14 +- .../alerts/fetch_elasticsearch_versions.ts | 19 +- .../lib/alerts/fetch_index_shard_size.ts | 22 +- .../lib/alerts/fetch_kibana_versions.test.ts | 14 +- .../lib/alerts/fetch_kibana_versions.ts | 19 +- .../server/lib/alerts/fetch_licenses.test.ts | 12 + .../server/lib/alerts/fetch_licenses.ts | 19 +- .../alerts/fetch_logstash_versions.test.ts | 14 +- .../lib/alerts/fetch_logstash_versions.ts | 19 +- .../alerts/fetch_memory_usage_node_stats.ts | 19 +- .../fetch_missing_monitoring_data.test.ts | 17 +- .../alerts/fetch_missing_monitoring_data.ts | 13 +- .../alerts/fetch_nodes_from_cluster_stats.ts | 19 +- .../fetch_thread_pool_rejections_stats.ts | 19 +- .../server/lib/apm/create_apm_query.ts | 5 +- .../server/lib/apm/get_apms_for_clusters.ts | 21 +- .../server/lib/beats/create_beats_query.ts | 5 +- .../lib/beats/get_beats_for_clusters.ts | 19 +- .../lib/cluster/flag_supported_clusters.ts | 30 +-- .../server/lib/cluster/get_cluster_license.ts | 20 +- .../server/lib/cluster/get_cluster_stats.ts | 5 +- .../lib/cluster/get_clusters_from_request.ts | 39 +-- .../server/lib/cluster/get_clusters_state.ts | 18 +- .../lib/cluster/get_clusters_stats.test.js | 2 +- .../server/lib/cluster/get_clusters_stats.ts | 34 ++- .../lib/cluster/get_index_patterns.test.ts | 102 ++++++++ .../server/lib/cluster/get_index_patterns.ts | 89 ++++++- .../server/lib/create_query.test.js | 114 --------- .../server/lib/create_query.test.ts | 231 ++++++++++++++++++ .../monitoring/server/lib/create_query.ts | 38 ++- .../server/lib/details/get_metrics.test.js | 24 +- .../server/lib/details/get_metrics.ts | 7 +- .../server/lib/details/get_series.ts | 22 +- .../server/lib/elasticsearch/ccr.ts | 19 +- .../lib/elasticsearch/get_last_recovery.ts | 52 +++- .../server/lib/elasticsearch/get_ml_jobs.ts | 59 ++++- .../indices/get_index_summary.ts | 19 +- .../lib/elasticsearch/indices/get_indices.ts | 24 +- .../elasticsearch/nodes/get_node_summary.ts | 30 ++- .../nodes/get_nodes/get_node_ids.test.js | 15 ++ .../nodes/get_nodes/get_node_ids.ts | 16 +- .../nodes/get_nodes/get_nodes.ts | 22 +- .../get_nodes/get_paginated_nodes.test.js | 19 +- .../nodes/get_nodes/get_paginated_nodes.ts | 6 +- ...get_indices_unassigned_shard_stats.test.js | 16 +- .../get_indices_unassigned_shard_stats.ts | 29 ++- .../shards/get_nodes_shard_count.test.js | 16 +- .../shards/get_nodes_shard_count.ts | 33 +-- .../shards/get_shard_allocation.ts | 31 ++- .../elasticsearch/shards/get_shard_stats.ts | 20 +- .../shards/normalize_shard_objects.ts | 3 +- .../create_enterprise_search_query.ts | 22 +- .../get_enterprise_search_for_clusters.ts | 20 +- .../server/lib/enterprise_search/get_stats.ts | 20 +- .../server/lib/kibana/get_kibana_info.ts | 13 +- .../server/lib/kibana/get_kibanas.ts | 26 +- .../lib/kibana/get_kibanas_for_clusters.ts | 27 +- .../server/lib/logs/init_infra_source.ts | 2 +- .../server/lib/logstash/get_cluster_status.ts | 11 +- .../lib/logstash/get_logstash_for_clusters.ts | 25 +- .../server/lib/logstash/get_node_info.test.ts | 19 +- .../server/lib/logstash/get_node_info.ts | 17 +- .../server/lib/logstash/get_nodes.ts | 26 +- .../lib/logstash/get_paginated_pipelines.ts | 64 ++--- .../server/lib/logstash/get_pipeline.ts | 6 - .../server/lib/logstash/get_pipeline_ids.ts | 17 +- .../logstash/get_pipeline_state_document.ts | 19 +- .../get_pipeline_stats_aggregation.ts | 24 +- .../lib/logstash/get_pipeline_versions.ts | 22 +- .../lib/logstash/get_pipeline_vertex.ts | 6 - .../get_pipeline_vertex_stats_aggregation.ts | 23 +- .../collection/get_collection_status.test.js | 3 - .../setup/collection/get_collection_status.ts | 23 +- .../has_standalone_clusters.ts | 23 +- .../api/v1/apm/_get_apm_cluster_status.js | 4 +- .../server/routes/api/v1/apm/instance.js | 4 +- .../server/routes/api/v1/apm/overview.js | 8 +- .../server/routes/api/v1/beats/beat_detail.js | 4 +- .../server/routes/api/v1/beats/overview.js | 2 +- .../routes/api/v1/elasticsearch/ccr_shard.ts | 25 +- .../api/v1/elasticsearch/index_detail.js | 13 +- .../routes/api/v1/elasticsearch/indices.js | 19 +- .../routes/api/v1/elasticsearch/ml_jobs.js | 16 +- .../api/v1/elasticsearch/node_detail.js | 13 +- .../routes/api/v1/elasticsearch/nodes.js | 26 +- .../routes/api/v1/elasticsearch/overview.js | 36 +-- .../check/internal_monitoring.ts | 6 +- .../api/v1/enterprise_search/overview.js | 12 +- .../v1/kibana/_get_kibana_cluster_status.js | 6 +- .../server/routes/api/v1/kibana/instance.ts | 10 +- .../server/routes/api/v1/kibana/instances.js | 9 +- .../server/routes/api/v1/kibana/overview.js | 14 +- .../server/routes/api/v1/logstash/node.js | 13 +- .../server/routes/api/v1/logstash/nodes.js | 2 +- .../server/routes/api/v1/logstash/overview.js | 14 +- .../server/routes/api/v1/logstash/pipeline.js | 18 +- .../logstash/pipelines/cluster_pipelines.js | 9 +- .../v1/logstash/pipelines/node_pipelines.js | 9 +- .../apis/monitoring/apm/index.js | 6 +- .../apis/monitoring/beats/index.js | 6 +- .../apis/monitoring/cluster/index.js | 4 +- .../apis/monitoring/elasticsearch/index.js | 16 +- .../apis/monitoring/kibana/index.js | 6 +- .../apis/monitoring/logstash/index.js | 10 +- .../apis/monitoring/setup/collection/index.js | 8 +- .../data.json.gz | Bin 282109 -> 282241 bytes .../data.json.gz | Bin 229987 -> 229957 bytes 151 files changed, 1678 insertions(+), 1160 deletions(-) delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/append_mb_index.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/create_dataset_query_filter.ts create mode 100644 x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/create_query.test.js create mode 100644 x-pack/plugins/monitoring/server/lib/create_query.test.ts diff --git a/x-pack/plugins/monitoring/common/ccs_utils.ts b/x-pack/plugins/monitoring/common/ccs_utils.ts index 7efe6e43ddbbd..982189a1e4a97 100644 --- a/x-pack/plugins/monitoring/common/ccs_utils.ts +++ b/x-pack/plugins/monitoring/common/ccs_utils.ts @@ -13,32 +13,17 @@ type Config = Partial & { get?: (key: string) => any; }; -export function appendMetricbeatIndex( - config: Config, - indexPattern: string, - ccs?: string, - bypass: boolean = false -) { - if (bypass) { - return indexPattern; - } - // Leverage this function to also append the dynamic metricbeat index too - let mbIndex = null; +export function getConfigCcs(config: Config): boolean { + let ccsEnabled = false; // TODO: NP // This function is called with both NP config and LP config if (isFunction(config.get)) { - mbIndex = config.get('monitoring.ui.metricbeat.index'); + ccsEnabled = config.get('monitoring.ui.ccs.enabled'); } else { - mbIndex = get(config, 'ui.metricbeat.index'); - } - - if (ccs) { - mbIndex = `${mbIndex},${ccs}:${mbIndex}`; + ccsEnabled = get(config, 'ui.ccs.enabled'); } - - return `${indexPattern},${mbIndex}`; + return ccsEnabled; } - /** * Prefix all comma separated index patterns within the original {@code indexPattern}. * @@ -50,28 +35,10 @@ export function appendMetricbeatIndex( * @param {String} ccs The optional cluster-prefix to prepend. * @return {String} The index pattern with the {@code cluster} prefix appropriately prepended. */ -export function prefixIndexPattern( - config: Config, - indexPattern: string, - ccs?: string, - monitoringIndicesOnly: boolean = false -) { - let ccsEnabled = false; - // TODO: NP - // This function is called with both NP config and LP config - if (isFunction(config.get)) { - ccsEnabled = config.get('monitoring.ui.ccs.enabled'); - } else { - ccsEnabled = get(config, 'ui.ccs.enabled'); - } - +export function prefixIndexPattern(config: Config, indexPattern: string, ccs?: string) { + const ccsEnabled = getConfigCcs(config); if (!ccsEnabled || !ccs) { - return appendMetricbeatIndex( - config, - indexPattern, - ccsEnabled ? ccs : undefined, - monitoringIndicesOnly - ); + return indexPattern; } const patterns = indexPattern.split(','); @@ -79,15 +46,9 @@ export function prefixIndexPattern( // if a wildcard is used, then we also want to search the local indices if (ccs === '*') { - return appendMetricbeatIndex( - config, - `${prefixedPattern},${indexPattern}`, - ccs, - monitoringIndicesOnly - ); + return `${prefixedPattern},${indexPattern}`; } - - return appendMetricbeatIndex(config, prefixedPattern, ccs, monitoringIndicesOnly); + return prefixedPattern; } /** diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index 67cc86c013ca2..96f66fc3d4177 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -132,6 +132,9 @@ export const INDEX_PATTERN_ELASTICSEARCH = '.monitoring-es-*'; // ECS-compliant patterns (metricbeat >8 and agent) export const INDEX_PATTERN_ELASTICSEARCH_ECS = '.monitoring-es-8-*'; export const INDEX_PATTERN_ENTERPRISE_SEARCH = '.monitoring-ent-search-*'; +export const DS_INDEX_PATTERN_METRICS = 'metrics'; +export const DS_INDEX_PATTERN_LOGS = 'logs'; +export const DS_INDEX_PATTERN_ES = 'elasticsearch'; // This is the unique token that exists in monitoring indices collected by metricbeat export const METRICBEAT_INDEX_NAME_UNIQUE_TOKEN = '-mb-'; @@ -586,3 +589,12 @@ export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365', export const SAVED_OBJECT_TELEMETRY = 'monitoring-telemetry'; export const TELEMETRY_METRIC_BUTTON_CLICK = 'btnclick__'; + +export type INDEX_PATTERN_TYPES = + | 'elasticsearch' + | 'kibana' + | 'logstash' + | 'beats' + | 'enterprisesearch'; + +export type DS_INDEX_PATTERN_TYPES = typeof DS_INDEX_PATTERN_METRICS | typeof DS_INDEX_PATTERN_LOGS; diff --git a/x-pack/plugins/monitoring/server/alerts/base_rule.ts b/x-pack/plugins/monitoring/server/alerts/base_rule.ts index f05077ec4bb00..d13e6d9ed7f9b 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_rule.ts @@ -28,10 +28,7 @@ import { CommonAlertParams, } from '../../common/types/alerts'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { parseDuration } from '../../../alerting/common'; import { Globals } from '../static_globals'; @@ -226,23 +223,14 @@ export class BaseRule { ); const esClient = services.scopedClusterClient.asCurrentUser; - const availableCcs = Globals.app.config.ui.ccs.enabled; - const clusters = await this.fetchClusters(esClient, params as CommonAlertParams, availableCcs); - const data = await this.fetchData(params, esClient, clusters, availableCcs); + const clusters = await this.fetchClusters(esClient, params as CommonAlertParams); + const data = await this.fetchData(params, esClient, clusters); return await this.processData(data, clusters, services, state); } - protected async fetchClusters( - esClient: ElasticsearchClient, - params: CommonAlertParams, - ccs?: boolean - ) { - let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); - if (ccs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, ccs); - } + protected async fetchClusters(esClient: ElasticsearchClient, params: CommonAlertParams) { if (!params.limit) { - return await fetchClusters(esClient, esIndexPattern); + return await fetchClusters(esClient); } const limit = parseDuration(params.limit); const rangeFilter = this.ruleOptions.fetchClustersRange @@ -253,14 +241,13 @@ export class BaseRule { }, } : undefined; - return await fetchClusters(esClient, esIndexPattern, rangeFilter); + return await fetchClusters(esClient, rangeFilter); } protected async fetchData( params: CommonAlertParams | unknown, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise> { throw new Error('Child classes must implement `fetchData`'); } diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts index 8dd4623bfd7e4..ed4ba69b8e254 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts @@ -39,7 +39,6 @@ jest.mock('../static_globals', () => ({ config: { ui: { ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, container: { elasticsearch: { enabled: false } }, }, }, diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts index 9b2f9b9fb3ed7..705d0c6b9c87f 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts @@ -22,18 +22,12 @@ import { CCRReadExceptionsStats, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - INDEX_PATTERN_ELASTICSEARCH, - RULE_CCR_READ_EXCEPTIONS, - RULE_DETAILS, -} from '../../common/constants'; +import { RULE_CCR_READ_EXCEPTIONS, RULE_DETAILS } from '../../common/constants'; import { fetchCCRReadExceptions } from '../lib/alerts/fetch_ccr_read_exceptions'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { parseDuration } from '../../../alerting/common/parse_duration'; import { SanitizedAlert, RawAlertInstance } from '../../../alerting/common'; import { AlertingDefaults, createLink } from './alert_helpers'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; export class CCRReadExceptionsRule extends BaseRule { @@ -72,20 +66,14 @@ export class CCRReadExceptionsRule extends BaseRule { protected async fetchData( params: CommonAlertParams, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise { - let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); - if (availableCcs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } const { duration: durationString } = params; const duration = parseDuration(durationString); const endMs = +new Date(); const startMs = endMs - duration; const stats = await fetchCCRReadExceptions( esClient, - esIndexPattern, startMs, endMs, Globals.app.config.ui.max_bucket_size, diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts index 5d209f7fc4a81..85030657825c4 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts @@ -21,7 +21,6 @@ jest.mock('../static_globals', () => ({ config: { ui: { ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, }, }, }, diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts index e85fb33cd76bd..b8810196c833a 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts @@ -19,17 +19,10 @@ import { AlertInstanceState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - RULE_CLUSTER_HEALTH, - LEGACY_RULE_DETAILS, - INDEX_PATTERN_ELASTICSEARCH, -} from '../../common/constants'; +import { RULE_CLUSTER_HEALTH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertMessageTokenType, AlertClusterHealthType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerting/common'; -import { Globals } from '../static_globals'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health'; const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterHealth.redMessage', { @@ -66,19 +59,9 @@ export class ClusterHealthRule extends BaseRule { protected async fetchData( params: CommonAlertParams, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise { - let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); - if (availableCcs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } - const healths = await fetchClusterHealth( - esClient, - clusters, - esIndexPattern, - params.filterQuery - ); + const healths = await fetchClusterHealth(esClient, clusters, params.filterQuery); return healths.map((clusterHealth) => { const shouldFire = clusterHealth.health !== AlertClusterHealthType.Green; const severity = diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts index 9b19c1ddeb7d1..bcd2c0cbb5810 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts @@ -27,7 +27,6 @@ jest.mock('../static_globals', () => ({ config: { ui: { ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, container: { elasticsearch: { enabled: false } }, }, }, diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts index b41783d449c02..fa4b64fd997c3 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts @@ -23,16 +23,14 @@ import { CommonAlertFilter, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { INDEX_PATTERN_ELASTICSEARCH, RULE_CPU_USAGE, RULE_DETAILS } from '../../common/constants'; +import { RULE_CPU_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance, SanitizedAlert } from '../../../alerting/common'; import { parseDuration } from '../../../alerting/common/parse_duration'; import { AlertingDefaults, createLink } from './alert_helpers'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; export class CpuUsageRule extends BaseRule { @@ -60,20 +58,14 @@ export class CpuUsageRule extends BaseRule { protected async fetchData( params: CommonAlertParams, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise { - let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); - if (availableCcs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } const duration = parseDuration(params.duration); const endMs = +new Date(); const startMs = endMs - duration; const stats = await fetchCpuUsageNodeStats( esClient, clusters, - esIndexPattern, startMs, endMs, Globals.app.config.ui.max_bucket_size, diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts index 63ff6a7ccab93..daaded1c18c80 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts @@ -40,7 +40,6 @@ jest.mock('../static_globals', () => ({ config: { ui: { ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, container: { elasticsearch: { enabled: false } }, }, }, diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts index 17dff8ea6a9dd..1e06f0649d107 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts @@ -23,15 +23,13 @@ import { CommonAlertFilter, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { INDEX_PATTERN_ELASTICSEARCH, RULE_DISK_USAGE, RULE_DETAILS } from '../../common/constants'; +import { RULE_DISK_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance, SanitizedAlert } from '../../../alerting/common'; import { AlertingDefaults, createLink } from './alert_helpers'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; export class DiskUsageRule extends BaseRule { @@ -59,18 +57,12 @@ export class DiskUsageRule extends BaseRule { protected async fetchData( params: CommonAlertParams, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise { - let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); - if (availableCcs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } const { duration, threshold } = params; const stats = await fetchDiskUsageNodeStats( esClient, clusters, - esIndexPattern, duration as string, Globals.app.config.ui.max_bucket_size, params.filterQuery diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts index 12fa54f34e3c4..4531c5f0f1ffc 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts @@ -28,7 +28,6 @@ jest.mock('../static_globals', () => ({ config: { ui: { ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, container: { elasticsearch: { enabled: false } }, }, }, diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts index b873a20c874b5..9d89f827f9b10 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts @@ -18,17 +18,11 @@ import { AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - RULE_ELASTICSEARCH_VERSION_MISMATCH, - LEGACY_RULE_DETAILS, - INDEX_PATTERN_ELASTICSEARCH, -} from '../../common/constants'; +import { RULE_ELASTICSEARCH_VERSION_MISMATCH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerting/common'; import { Globals } from '../static_globals'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; export class ElasticsearchVersionMismatchRule extends BaseRule { @@ -55,17 +49,11 @@ export class ElasticsearchVersionMismatchRule extends BaseRule { protected async fetchData( params: CommonAlertParams, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise { - let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); - if (availableCcs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } const elasticsearchVersions = await fetchElasticsearchVersions( esClient, clusters, - esIndexPattern, Globals.app.config.ui.max_bucket_size, params.filterQuery ); diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts index 01016a7c02ae2..b4444c9088073 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts @@ -28,7 +28,6 @@ jest.mock('../static_globals', () => ({ config: { ui: { ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, container: { elasticsearch: { enabled: false } }, }, }, diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts index 79f449f8e7ef7..24182c4a545d3 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts @@ -18,17 +18,11 @@ import { AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - RULE_KIBANA_VERSION_MISMATCH, - LEGACY_RULE_DETAILS, - INDEX_PATTERN_KIBANA, -} from '../../common/constants'; +import { RULE_KIBANA_VERSION_MISMATCH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerting/common'; import { Globals } from '../static_globals'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; export class KibanaVersionMismatchRule extends BaseRule { @@ -68,17 +62,11 @@ export class KibanaVersionMismatchRule extends BaseRule { protected async fetchData( params: CommonAlertParams, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise { - let kibanaIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_KIBANA); - if (availableCcs) { - kibanaIndexPattern = getCcsIndexPattern(kibanaIndexPattern, availableCcs); - } const kibanaVersions = await fetchKibanaVersions( esClient, clusters, - kibanaIndexPattern, Globals.app.config.ui.max_bucket_size, params.filterQuery ); diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts index f7d6081edd306..0460064b4f7c5 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts @@ -40,7 +40,6 @@ jest.mock('../static_globals', () => ({ config: { ui: { ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, container: { elasticsearch: { enabled: false } }, }, }, diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts index 3009995e2f292..92be43b9c06c0 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts @@ -22,17 +22,11 @@ import { IndexShardSizeStats, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - INDEX_PATTERN_ELASTICSEARCH, - RULE_LARGE_SHARD_SIZE, - RULE_DETAILS, -} from '../../common/constants'; +import { RULE_LARGE_SHARD_SIZE, RULE_DETAILS } from '../../common/constants'; import { fetchIndexShardSize } from '../lib/alerts/fetch_index_shard_size'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { SanitizedAlert, RawAlertInstance } from '../../../alerting/common'; import { AlertingDefaults, createLink } from './alert_helpers'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; export class LargeShardSizeRule extends BaseRule { @@ -60,19 +54,13 @@ export class LargeShardSizeRule extends BaseRule { protected async fetchData( params: CommonAlertParams & { indexPattern: string }, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise { - let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); - if (availableCcs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } const { threshold, indexPattern: shardIndexPatterns } = params; const stats = await fetchIndexShardSize( esClient, clusters, - esIndexPattern, threshold!, shardIndexPatterns, Globals.app.config.ui.max_bucket_size, diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts index b29a5fc4661d7..86a6f666fcf87 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts @@ -34,7 +34,6 @@ jest.mock('../static_globals', () => ({ ui: { show_license_expiration: true, ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, container: { elasticsearch: { enabled: false } }, }, }, diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts index fc050bd678012..3a837a125a523 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts @@ -21,17 +21,11 @@ import { AlertLicenseState, } from '../../common/types/alerts'; import { AlertExecutorOptions, AlertInstance } from '../../../alerting/server'; -import { - RULE_LICENSE_EXPIRATION, - LEGACY_RULE_DETAILS, - INDEX_PATTERN_ELASTICSEARCH, -} from '../../common/constants'; +import { RULE_LICENSE_EXPIRATION, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerting/common'; import { Globals } from '../static_globals'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchLicenses } from '../lib/alerts/fetch_licenses'; const EXPIRES_DAYS = [60, 30, 14, 7]; @@ -80,14 +74,9 @@ export class LicenseExpirationRule extends BaseRule { protected async fetchData( params: CommonAlertParams, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise { - let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); - if (availableCcs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } - const licenses = await fetchLicenses(esClient, clusters, esIndexPattern, params.filterQuery); + const licenses = await fetchLicenses(esClient, clusters, params.filterQuery); return licenses.map((license) => { const { clusterUuid, type, expiryDateMS, status, ccs } = license; diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts index 20f64b65ba1f0..857a9bf5bfa79 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts @@ -29,7 +29,6 @@ jest.mock('../static_globals', () => ({ ui: { show_license_expiration: true, ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, container: { elasticsearch: { enabled: false } }, }, }, diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts index 6d7c06c1c1e07..ee3e5838d7d35 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts @@ -18,17 +18,11 @@ import { AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - RULE_LOGSTASH_VERSION_MISMATCH, - LEGACY_RULE_DETAILS, - INDEX_PATTERN_LOGSTASH, -} from '../../common/constants'; +import { RULE_LOGSTASH_VERSION_MISMATCH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerting/common'; import { Globals } from '../static_globals'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; export class LogstashVersionMismatchRule extends BaseRule { @@ -55,17 +49,11 @@ export class LogstashVersionMismatchRule extends BaseRule { protected async fetchData( params: CommonAlertParams, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise { - let logstashIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_LOGSTASH); - if (availableCcs) { - logstashIndexPattern = getCcsIndexPattern(logstashIndexPattern, availableCcs); - } const logstashVersions = await fetchLogstashVersions( esClient, clusters, - logstashIndexPattern, Globals.app.config.ui.max_bucket_size, params.filterQuery ); diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts index 8547f126a02d6..6e7aff2ae8fb4 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts @@ -27,7 +27,6 @@ jest.mock('../static_globals', () => ({ config: { ui: { ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, container: { elasticsearch: { enabled: false } }, }, }, diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts index 25b12379f8d3a..06ecf4bb450c8 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts @@ -23,19 +23,13 @@ import { CommonAlertFilter, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - INDEX_PATTERN_ELASTICSEARCH, - RULE_MEMORY_USAGE, - RULE_DETAILS, -} from '../../common/constants'; +import { RULE_MEMORY_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; import { fetchMemoryUsageNodeStats } from '../lib/alerts/fetch_memory_usage_node_stats'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance, SanitizedAlert } from '../../../alerting/common'; import { AlertingDefaults, createLink } from './alert_helpers'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { parseDuration } from '../../../alerting/common/parse_duration'; import { Globals } from '../static_globals'; @@ -64,13 +58,8 @@ export class MemoryUsageRule extends BaseRule { protected async fetchData( params: CommonAlertParams, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise { - let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); - if (availableCcs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } const { duration, threshold } = params; const parsedDuration = parseDuration(duration as string); const endMs = +new Date(); @@ -79,7 +68,6 @@ export class MemoryUsageRule extends BaseRule { const stats = await fetchMemoryUsageNodeStats( esClient, clusters, - esIndexPattern, startMs, endMs, Globals.app.config.ui.max_bucket_size, diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts index 66259b9fffac9..a8a96a61a4b25 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts @@ -29,7 +29,6 @@ jest.mock('../static_globals', () => ({ ui: { show_license_expiration: true, ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, container: { elasticsearch: { enabled: false } }, }, }, diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts index 50ba9fa9e3586..fa7cbe009712a 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts @@ -20,12 +20,10 @@ import { AlertNodeState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { INDEX_PATTERN, RULE_MISSING_MONITORING_DATA, RULE_DETAILS } from '../../common/constants'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { RULE_MISSING_MONITORING_DATA, RULE_DETAILS } from '../../common/constants'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance, SanitizedAlert } from '../../../alerting/common'; import { parseDuration } from '../../../alerting/common/parse_duration'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data'; import { AlertingDefaults, createLink } from './alert_helpers'; import { Globals } from '../static_globals'; @@ -59,20 +57,14 @@ export class MissingMonitoringDataRule extends BaseRule { protected async fetchData( params: CommonAlertParams, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise { - let indexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN); - if (availableCcs) { - indexPattern = getCcsIndexPattern(indexPattern, availableCcs); - } const duration = parseDuration(params.duration); const limit = parseDuration(params.limit!); const now = +new Date(); const missingData = await fetchMissingMonitoringData( esClient, clusters, - indexPattern, Globals.app.config.ui.max_bucket_size, now, now - limit - LIMIT_BUFFER, diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts index 5145a4e8476af..3e24df3a6ef15 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts @@ -34,7 +34,6 @@ jest.mock('../static_globals', () => ({ config: { ui: { ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, container: { elasticsearch: { enabled: false } }, }, }, diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts index 545d6331d225e..82cf91e91b52a 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts @@ -20,19 +20,11 @@ import { AlertNodesChangedState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - RULE_NODES_CHANGED, - LEGACY_RULE_DETAILS, - INDEX_PATTERN_ELASTICSEARCH, -} from '../../common/constants'; +import { RULE_NODES_CHANGED, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerting/common'; -import { Globals } from '../static_globals'; import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { AlertSeverity } from '../../common/enums'; - interface AlertNodesChangedStates { removed: AlertClusterStatsNode[]; added: AlertClusterStatsNode[]; @@ -104,17 +96,11 @@ export class NodesChangedRule extends BaseRule { protected async fetchData( params: CommonAlertParams, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise { - let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); - if (availableCcs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } const nodesFromClusterStats = await fetchNodesFromClusterStats( esClient, clusters, - esIndexPattern, params.filterQuery ); return nodesFromClusterStats.map((nodes) => { diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts index e6c2002eaff87..0cca5eb81c95f 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts @@ -21,13 +21,10 @@ import { AlertThreadPoolRejectionsStats, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats'; -import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { Alert, RawAlertInstance } from '../../../alerting/common'; import { AlertingDefaults, createLink } from './alert_helpers'; -import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; type ActionVariables = Array<{ name: string; description: string }>; @@ -70,20 +67,13 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { protected async fetchData( params: ThreadPoolRejectionsAlertParams, esClient: ElasticsearchClient, - clusters: AlertCluster[], - availableCcs: boolean + clusters: AlertCluster[] ): Promise { - let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); - if (availableCcs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } - const { threshold, duration } = params; const stats = await fetchThreadPoolRejectionStats( esClient, clusters, - esIndexPattern, Globals.app.config.ui.max_bucket_size, this.threadPoolType, duration, diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts index 351980d3f385d..63a02088b9b65 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts @@ -29,7 +29,6 @@ jest.mock('../static_globals', () => ({ ui: { show_license_expiration: true, ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, container: { elasticsearch: { enabled: false } }, }, }, diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts index 79896d11da2c3..da4c7ffaeffa0 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts @@ -29,7 +29,6 @@ jest.mock('../static_globals', () => ({ ui: { show_license_expiration: true, ccs: { enabled: true }, - metricbeat: { index: 'metricbeat-*' }, container: { elasticsearch: { enabled: false } }, }, }, diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts index 036ade38607e9..22e7b74368ebf 100644 --- a/x-pack/plugins/monitoring/server/config.test.ts +++ b/x-pack/plugins/monitoring/server/config.test.ts @@ -102,9 +102,6 @@ describe('config schema', () => { "index": "filebeat-*", }, "max_bucket_size": 10000, - "metricbeat": Object { - "index": "metricbeat-*", - }, "min_interval_seconds": 10, "show_license_expiration": true, }, diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts index 835ad30de7cbe..3facfd97319f2 100644 --- a/x-pack/plugins/monitoring/server/config.ts +++ b/x-pack/plugins/monitoring/server/config.ts @@ -32,9 +32,6 @@ export const configSchema = schema.object({ logs: schema.object({ index: schema.string({ defaultValue: 'filebeat-*' }), }), - metricbeat: schema.object({ - index: schema.string({ defaultValue: 'metricbeat-*' }), - }), max_bucket_size: schema.number({ defaultValue: 10000 }), elasticsearch: monitoringElasticsearchConfigSchema, container: schema.object({ diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts index 4c454637bf8bb..cbbfe64f5e3e2 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts @@ -11,8 +11,6 @@ import { MonitoringConfig } from '../../config'; import { getStackProductsUsage } from './lib/get_stack_products_usage'; import { fetchLicenseType } from './lib/fetch_license_type'; import { MonitoringUsage, StackProductUsage, MonitoringClusterStackProductUsage } from './types'; -import { INDEX_PATTERN_ELASTICSEARCH } from '../../../common/constants'; -import { getCcsIndexPattern } from '../../lib/alerts/get_ccs_index_pattern'; import { fetchClusters } from '../../lib/alerts/fetch_clusters'; export function getMonitoringUsageCollector( @@ -106,8 +104,7 @@ export function getMonitoringUsageCollector( : getClient().asInternalUser; const usageClusters: MonitoringClusterStackProductUsage[] = []; const availableCcs = config.ui.ccs.enabled; - const elasticsearchIndex = getCcsIndexPattern(INDEX_PATTERN_ELASTICSEARCH, availableCcs); - const clusters = await fetchClusters(callCluster, elasticsearchIndex); + const clusters = await fetchClusters(callCluster); for (const cluster of clusters) { const license = await fetchLicenseType(callCluster, availableCcs, cluster.clusterUuid); const stackProducts = await getStackProductsUsage( diff --git a/x-pack/plugins/monitoring/server/lib/alerts/append_mb_index.ts b/x-pack/plugins/monitoring/server/lib/alerts/append_mb_index.ts deleted file mode 100644 index c8713f70ea5cf..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/append_mb_index.ts +++ /dev/null @@ -1,12 +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 { MonitoringConfig } from '../../config'; - -export function appendMetricbeatIndex(config: MonitoringConfig, indexPattern: string) { - return `${indexPattern},${config.ui.metricbeat.index}`; -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/create_dataset_query_filter.ts b/x-pack/plugins/monitoring/server/lib/alerts/create_dataset_query_filter.ts new file mode 100644 index 0000000000000..7d23240bc1e94 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/create_dataset_query_filter.ts @@ -0,0 +1,24 @@ +/* + * 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 const createDatasetFilter = (legacyType: string, dataset: string) => ({ + bool: { + should: [ + { + term: { + type: legacyType, + }, + }, + { + term: { + 'data_stream.dataset': dataset, + }, + }, + ], + minimum_should_match: 1, + }, +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts index e7a5923207d60..d9dd035b92c4d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts @@ -8,17 +8,26 @@ import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { CCRReadExceptionsStats } from '../../../common/types/alerts'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { createDatasetFilter } from './create_dataset_query_filter'; +import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; export async function fetchCCRReadExceptions( esClient: ElasticsearchClient, - index: string, startMs: number, endMs: number, size: number, filterQuery?: string ): Promise { + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'elasticsearch', + dataset: 'ccr', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: ['aggregations.remote_clusters.buckets'], body: { size: 0, @@ -35,11 +44,7 @@ export async function fetchCCRReadExceptions( }, }, }, - { - term: { - type: 'ccr_stats', - }, - }, + createDatasetFilter('ccr_stats', 'elasticsearch.ccr'), { range: { timestamp: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts index 2739e23245bde..ea87608a14ef0 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts @@ -10,6 +10,18 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; import { fetchClusterHealth } from './fetch_cluster_health'; +jest.mock('../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + describe('fetchClusterHealth', () => { it('should return the cluster health', async () => { const status = 'green'; @@ -34,9 +46,8 @@ describe('fetchClusterHealth', () => { ); const clusters = [{ clusterUuid, clusterName: 'foo' }]; - const index = '.monitoring-es-*'; - const health = await fetchClusterHealth(esClient, clusters, index); + const health = await fetchClusterHealth(esClient, clusters); expect(health).toEqual([ { health: status, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts index b2004f0c7c710..59fd379344b4c 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts @@ -7,15 +7,24 @@ import { ElasticsearchClient } from 'kibana/server'; import { AlertCluster, AlertClusterHealth } from '../../../common/types/alerts'; import { ElasticsearchSource, ElasticsearchResponse } from '../../../common/types/es'; +import { createDatasetFilter } from './create_dataset_query_filter'; +import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; export async function fetchClusterHealth( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string, filterQuery?: string ): Promise { + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'elasticsearch', + dataset: 'cluster_stats', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: [ 'hits.hits._source.cluster_state.status', 'hits.hits._source.cluster_uuid', @@ -39,11 +48,7 @@ export async function fetchClusterHealth( cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), }, }, - { - term: { - type: 'cluster_stats', - }, - }, + createDatasetFilter('cluster_stats', 'elasticsearch.cluster_stats'), { range: { timestamp: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts index c46ec424b2cd3..fa4fa0269fb1b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts @@ -11,6 +11,18 @@ import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mo import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { fetchClusters } from './fetch_clusters'; +jest.mock('../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + describe('fetchClusters', () => { const clusterUuid = '1sdfds734'; const clusterName = 'monitoring'; @@ -31,8 +43,7 @@ describe('fetchClusters', () => { }, } as estypes.SearchResponse) ); - const index = '.monitoring-es-*'; - const result = await fetchClusters(esClient, index); + const result = await fetchClusters(esClient); expect(result).toEqual([{ clusterUuid, clusterName }]); }); @@ -60,15 +71,13 @@ describe('fetchClusters', () => { }, } as estypes.SearchResponse) ); - const index = '.monitoring-es-*'; - const result = await fetchClusters(esClient, index); + const result = await fetchClusters(esClient); expect(result).toEqual([{ clusterUuid, clusterName: metadataName }]); }); it('should limit the time period in the query', async () => { const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; - const index = '.monitoring-es-*'; - await fetchClusters(esClient, index); + await fetchClusters(esClient); const params = esClient.search.mock.calls[0][0] as any; expect(params?.body?.query.bool.filter[1].range.timestamp.gte).toBe('now-2m'); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts index 984cdee4e1b7b..76bd81c24145f 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -8,6 +8,10 @@ import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster } from '../../../common/types/alerts'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { createDatasetFilter } from './create_dataset_query_filter'; +import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; interface RangeFilter { [field: string]: { @@ -18,11 +22,15 @@ interface RangeFilter { export async function fetchClusters( esClient: ElasticsearchClient, - index: string, rangeFilter: RangeFilter = { timestamp: { gte: 'now-2m' } } ): Promise { + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'elasticsearch', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: [ 'hits.hits._source.cluster_settings.cluster.metadata.display_name', 'hits.hits._source.cluster_uuid', @@ -33,11 +41,7 @@ export async function fetchClusters( query: { bool: { filter: [ - { - term: { - type: 'cluster_stats', - }, - }, + createDatasetFilter('cluster_stats', 'elasticsearch.cluster_stats'), { range: rangeFilter, }, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts index a67a5e679cc33..fd7b61b5f92ff 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts @@ -10,6 +10,18 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; import { fetchCpuUsageNodeStats } from './fetch_cpu_usage_node_stats'; +jest.mock('../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + describe('fetchCpuUsageNodeStats', () => { const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; const clusters = [ @@ -18,7 +30,6 @@ describe('fetchCpuUsageNodeStats', () => { clusterName: 'test', }, ]; - const index = '.monitoring-es-*'; const startMs = 0; const endMs = 0; const size = 10; @@ -62,7 +73,7 @@ describe('fetchCpuUsageNodeStats', () => { }, }) ); - const result = await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size); + const result = await fetchCpuUsageNodeStats(esClient, clusters, startMs, endMs, size); expect(result).toEqual([ { clusterUuid: clusters[0].clusterUuid, @@ -129,7 +140,7 @@ describe('fetchCpuUsageNodeStats', () => { }, }) ); - const result = await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size); + const result = await fetchCpuUsageNodeStats(esClient, clusters, startMs, endMs, size); expect(result).toEqual([ { clusterUuid: clusters[0].clusterUuid, @@ -189,7 +200,7 @@ describe('fetchCpuUsageNodeStats', () => { }, }) ); - const result = await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size); + const result = await fetchCpuUsageNodeStats(esClient, clusters, startMs, endMs, size); expect(result[0].ccs).toBe('foo'); }); @@ -203,9 +214,10 @@ describe('fetchCpuUsageNodeStats', () => { }); const filterQuery = '{"bool":{"should":[{"exists":{"field":"cluster_uuid"}}],"minimum_should_match":1}}'; - await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size, filterQuery); + await fetchCpuUsageNodeStats(esClient, clusters, startMs, endMs, size, filterQuery); expect(params).toStrictEqual({ - index: '.monitoring-es-*', + index: + '*:.monitoring-es-*,.monitoring-es-*,*:metrics-elasticsearch.node_stats-*,metrics-elasticsearch.node_stats-*', filter_path: ['aggregations'], body: { size: 0, @@ -213,7 +225,15 @@ describe('fetchCpuUsageNodeStats', () => { bool: { filter: [ { terms: { cluster_uuid: ['abc123'] } }, - { term: { type: 'node_stats' } }, + { + bool: { + should: [ + { term: { type: 'node_stats' } }, + { term: { 'data_stream.dataset': 'elasticsearch.node_stats' } }, + ], + minimum_should_match: 1, + }, + }, { range: { timestamp: { format: 'epoch_millis', gte: 0, lte: 0 } } }, { bool: { should: [{ exists: { field: 'cluster_uuid' } }], minimum_should_match: 1 }, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts index 2ad42870e9958..99fc9db7d097b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts @@ -10,6 +10,10 @@ import { get } from 'lodash'; import moment from 'moment'; import { NORMALIZED_DERIVATIVE_UNIT } from '../../../common/constants'; import { AlertCluster, AlertCpuUsageNodeStats } from '../../../common/types/alerts'; +import { createDatasetFilter } from './create_dataset_query_filter'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; interface NodeBucketESResponse { key: string; @@ -26,7 +30,6 @@ interface ClusterBucketESResponse { export async function fetchCpuUsageNodeStats( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string, startMs: number, endMs: number, size: number, @@ -35,8 +38,15 @@ export async function fetchCpuUsageNodeStats( // Using pure MS didn't seem to work well with the date_histogram interval // but minutes does const intervalInMinutes = moment.duration(endMs - startMs).asMinutes(); + + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'elasticsearch', + dataset: 'node_stats', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: ['aggregations'], body: { size: 0, @@ -48,11 +58,7 @@ export async function fetchCpuUsageNodeStats( cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), }, }, - { - term: { - type: 'node_stats', - }, - }, + createDatasetFilter('node_stats', 'elasticsearch.node_stats'), { range: { timestamp: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts index 4766400891af5..d4bbbe71e9c41 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts @@ -10,6 +10,18 @@ import { elasticsearchClientMock } from '../../../../../../src/core/server/elast import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { fetchDiskUsageNodeStats } from './fetch_disk_usage_node_stats'; +jest.mock('../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + describe('fetchDiskUsageNodeStats', () => { const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; @@ -19,7 +31,6 @@ describe('fetchDiskUsageNodeStats', () => { clusterName: 'test-cluster', }, ]; - const index = '.monitoring-es-*'; const duration = '5m'; const size = 10; @@ -63,7 +74,7 @@ describe('fetchDiskUsageNodeStats', () => { }) ); - const result = await fetchDiskUsageNodeStats(esClient, clusters, index, duration, size); + const result = await fetchDiskUsageNodeStats(esClient, clusters, duration, size); expect(result).toEqual([ { clusterUuid: clusters[0].clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts index 2d4872c0bd895..9116d47727854 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts @@ -8,18 +8,27 @@ import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster, AlertDiskUsageNodeStats } from '../../../common/types/alerts'; +import { createDatasetFilter } from './create_dataset_query_filter'; +import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; export async function fetchDiskUsageNodeStats( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string, duration: string, size: number, filterQuery?: string ): Promise { const clustersIds = clusters.map((cluster) => cluster.clusterUuid); + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'elasticsearch', + dataset: 'node_stats', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: ['aggregations'], body: { size: 0, @@ -31,11 +40,7 @@ export async function fetchDiskUsageNodeStats( cluster_uuid: clustersIds, }, }, - { - term: { - type: 'node_stats', - }, - }, + createDatasetFilter('node_stats', 'elasticsearch.node_stats'), { range: { timestamp: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts index 515fa3b2442d3..c8505a82614db 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts @@ -11,6 +11,18 @@ import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { fetchElasticsearchVersions } from './fetch_elasticsearch_versions'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +jest.mock('../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + describe('fetchElasticsearchVersions', () => { const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; @@ -45,7 +57,7 @@ describe('fetchElasticsearchVersions', () => { } as estypes.SearchResponse) ); - const result = await fetchElasticsearchVersions(esClient, clusters, index, size); + const result = await fetchElasticsearchVersions(esClient, clusters, size); expect(result).toEqual([ { clusterUuid: clusters[0].clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts index 6ca2e89048df9..16c7f67851f3d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts @@ -7,16 +7,25 @@ import { ElasticsearchClient } from 'kibana/server'; import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; import { ElasticsearchSource, ElasticsearchResponse } from '../../../common/types/es'; +import { createDatasetFilter } from './create_dataset_query_filter'; +import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; export async function fetchElasticsearchVersions( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string, size: number, filterQuery?: string ): Promise { + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'elasticsearch', + dataset: 'cluster_stats', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: [ 'hits.hits._source.cluster_stats.nodes.versions', 'hits.hits._index', @@ -40,11 +49,7 @@ export async function fetchElasticsearchVersions( cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), }, }, - { - term: { - type: 'cluster_stats', - }, - }, + createDatasetFilter('cluster_stats', 'elasticsearch.cluster_stats'), { range: { timestamp: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts index 9259adc63e546..65ed31f5db510 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts @@ -9,7 +9,10 @@ import { ElasticsearchClient } from 'kibana/server'; import { AlertCluster, IndexShardSizeStats } from '../../../common/types/alerts'; import { ElasticsearchIndexStats, ElasticsearchResponseHit } from '../../../common/types/es'; import { ESGlobPatterns, RegExPatterns } from '../../../common/es_glob_patterns'; +import { createDatasetFilter } from './create_dataset_query_filter'; import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; type TopHitType = ElasticsearchResponseHit & { _source: { index_stats?: Partial }; @@ -28,25 +31,26 @@ const gbMultiplier = 1000000000; export async function fetchIndexShardSize( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string, threshold: number, shardIndexPatterns: string, size: number, filterQuery?: string ): Promise { + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'elasticsearch', + dataset: 'index', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: ['aggregations.clusters.buckets'], body: { size: 0, query: { bool: { - must: [ - { - match: { - type: 'index_stats', - }, - }, + filter: [ + createDatasetFilter('index_stats', 'elasticsearch.index'), { range: { timestamp: { @@ -102,7 +106,7 @@ export async function fetchIndexShardSize( try { if (filterQuery) { const filterQueryObject = JSON.parse(filterQuery); - params.body.query.bool.must.push(filterQueryObject); + params.body.query.bool.filter.push(filterQueryObject); } } catch (e) { // meh diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts index f9a03bb73d5fc..6bdecaf6f83ed 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts @@ -10,6 +10,18 @@ import { fetchKibanaVersions } from './fetch_kibana_versions'; import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; +jest.mock('../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + describe('fetchKibanaVersions', () => { const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; const clusters = [ @@ -66,7 +78,7 @@ describe('fetchKibanaVersions', () => { }) ); - const result = await fetchKibanaVersions(esClient, clusters, index, size); + const result = await fetchKibanaVersions(esClient, clusters, size); expect(result).toEqual([ { clusterUuid: clusters[0].clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts index 71813f3a526de..48465954e8afb 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts @@ -7,6 +7,10 @@ import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; +import { createDatasetFilter } from './create_dataset_query_filter'; +import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; interface ESAggResponse { key: string; @@ -15,12 +19,17 @@ interface ESAggResponse { export async function fetchKibanaVersions( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string, size: number, filterQuery?: string ): Promise { + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'kibana', + dataset: 'stats', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: ['aggregations'], body: { size: 0, @@ -32,11 +41,7 @@ export async function fetchKibanaVersions( cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), }, }, - { - term: { - type: 'kibana_stats', - }, - }, + createDatasetFilter('kibana_stats', 'kibana.stats'), { range: { timestamp: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts index 538e24a764276..fbfe6ba58d540 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -10,6 +10,18 @@ import { elasticsearchClientMock } from '../../../../../../src/core/server/elast import { elasticsearchServiceMock } from 'src/core/server/mocks'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +jest.mock('../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + describe('fetchLicenses', () => { const clusterName = 'MyCluster'; const clusterUuid = 'clusterA'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts index b7bdf2fb6be72..0d21227112ecf 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -7,15 +7,24 @@ import { ElasticsearchClient } from 'kibana/server'; import { AlertLicense, AlertCluster } from '../../../common/types/alerts'; import { ElasticsearchSource } from '../../../common/types/es'; +import { createDatasetFilter } from './create_dataset_query_filter'; +import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; export async function fetchLicenses( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string, filterQuery?: string ): Promise { + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'elasticsearch', + dataset: 'cluster_stats', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: [ 'hits.hits._source.license.*', 'hits.hits._source.cluster_uuid', @@ -39,11 +48,7 @@ export async function fetchLicenses( cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), }, }, - { - term: { - type: 'cluster_stats', - }, - }, + createDatasetFilter('cluster_stats', 'elasticsearch.cluster_stats'), { range: { timestamp: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts index 5732fc00f009b..a1df1a56ad3b5 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts @@ -10,6 +10,18 @@ import { fetchLogstashVersions } from './fetch_logstash_versions'; import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; +jest.mock('../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + describe('fetchLogstashVersions', () => { const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; const clusters = [ @@ -66,7 +78,7 @@ describe('fetchLogstashVersions', () => { }) ); - const result = await fetchLogstashVersions(esClient, clusters, index, size); + const result = await fetchLogstashVersions(esClient, clusters, size); expect(result).toEqual([ { clusterUuid: clusters[0].clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts index 112c2fe798b10..3527223c3b07a 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts @@ -7,6 +7,10 @@ import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; +import { createDatasetFilter } from './create_dataset_query_filter'; +import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; interface ESAggResponse { key: string; @@ -15,12 +19,17 @@ interface ESAggResponse { export async function fetchLogstashVersions( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string, size: number, filterQuery?: string ): Promise { + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'logstash', + dataset: 'node_stats', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: ['aggregations'], body: { size: 0, @@ -32,11 +41,7 @@ export async function fetchLogstashVersions( cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), }, }, - { - term: { - type: 'logstash_stats', - }, - }, + createDatasetFilter('logstash_stats', 'logstash.node_stats'), { range: { timestamp: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts index 9403ae5d79a70..96462462ada6b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts @@ -8,19 +8,28 @@ import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster, AlertMemoryUsageNodeStats } from '../../../common/types/alerts'; +import { createDatasetFilter } from './create_dataset_query_filter'; +import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; export async function fetchMemoryUsageNodeStats( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string, startMs: number, endMs: number, size: number, filterQuery?: string ): Promise { const clustersIds = clusters.map((cluster) => cluster.clusterUuid); + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'elasticsearch', + dataset: 'node_stats', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: ['aggregations'], body: { size: 0, @@ -32,11 +41,7 @@ export async function fetchMemoryUsageNodeStats( cluster_uuid: clustersIds, }, }, - { - term: { - type: 'node_stats', - }, - }, + createDatasetFilter('node_stats', 'elasticsearch.node_stats'), { range: { timestamp: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts index 980adb009ff8f..a51cced18fd7b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts @@ -9,6 +9,18 @@ import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; import { fetchMissingMonitoringData } from './fetch_missing_monitoring_data'; +jest.mock('../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + function getResponse( index: string, products: Array<{ @@ -42,7 +54,6 @@ function getResponse( describe('fetchMissingMonitoringData', () => { const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; - const index = '.monitoring-*'; const startMs = 100; const size = 10; @@ -87,7 +98,7 @@ describe('fetchMissingMonitoringData', () => { }, }) ); - const result = await fetchMissingMonitoringData(esClient, clusters, index, size, now, startMs); + const result = await fetchMissingMonitoringData(esClient, clusters, size, now, startMs); expect(result).toEqual([ { nodeId: 'nodeUuid1', @@ -137,7 +148,7 @@ describe('fetchMissingMonitoringData', () => { }, }) ); - const result = await fetchMissingMonitoringData(esClient, clusters, index, size, now, startMs); + const result = await fetchMissingMonitoringData(esClient, clusters, size, now, startMs); expect(result).toEqual([ { nodeId: 'nodeUuid1', diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts index cdf0f21b52b09..93ad44a5fd44b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts @@ -8,6 +8,9 @@ import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster, AlertMissingData } from '../../../common/types/alerts'; +import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; interface ClusterBucketESResponse { key: string; @@ -44,15 +47,21 @@ interface TopHitESResponse { export async function fetchMissingMonitoringData( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string, size: number, nowInMs: number, startMs: number, filterQuery?: string ): Promise { const endMs = nowInMs; + // changing this to only search ES because of changes related to https://github.com/elastic/kibana/issues/83309 + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'elasticsearch', + dataset: 'node_stats', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: ['aggregations.clusters.buckets'], body: { size: 0, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts index 3dc3e315318fc..b67d071afcc0d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -7,6 +7,10 @@ import { ElasticsearchClient } from 'kibana/server'; import { AlertCluster, AlertClusterStatsNodes } from '../../../common/types/alerts'; import { ElasticsearchSource } from '../../../common/types/es'; +import { createDatasetFilter } from './create_dataset_query_filter'; +import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; function formatNode( nodes: NonNullable['nodes']> | undefined @@ -26,11 +30,16 @@ function formatNode( export async function fetchNodesFromClusterStats( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string, filterQuery?: string ): Promise { + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'elasticsearch', + dataset: 'cluster_stats', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: ['aggregations.clusters.buckets'], body: { size: 0, @@ -45,11 +54,7 @@ export async function fetchNodesFromClusterStats( query: { bool: { filter: [ - { - term: { - type: 'cluster_stats', - }, - }, + createDatasetFilter('cluster_stats', 'elasticsearch.cluster_stats'), { range: { timestamp: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts index 0d1d052b5f866..472bf0e955a2d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts @@ -8,6 +8,10 @@ import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster, AlertThreadPoolRejectionsStats } from '../../../common/types/alerts'; +import { createDatasetFilter } from './create_dataset_query_filter'; +import { Globals } from '../../static_globals'; +import { getConfigCcs } from '../../../common/ccs_utils'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; const invalidNumberValue = (value: number) => { return isNaN(value) || value === undefined || value === null; @@ -33,15 +37,20 @@ const getTopHits = (threadType: string, order: 'asc' | 'desc') => ({ export async function fetchThreadPoolRejectionStats( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string, size: number, threadType: string, duration: string, filterQuery?: string ): Promise { const clustersIds = clusters.map((cluster) => cluster.clusterUuid); + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'elasticsearch', + dataset: 'node_stats', + ccs: getConfigCcs(Globals.app.config) ? '*' : undefined, + }); const params = { - index, + index: indexPatterns, filter_path: ['aggregations'], body: { size: 0, @@ -53,11 +62,7 @@ export async function fetchThreadPoolRejectionStats( cluster_uuid: clustersIds, }, }, - { - term: { - type: 'node_stats', - }, - }, + createDatasetFilter('node_stats', 'elasticsearch.node_stats'), { range: { timestamp: { diff --git a/x-pack/plugins/monitoring/server/lib/apm/create_apm_query.ts b/x-pack/plugins/monitoring/server/lib/apm/create_apm_query.ts index 63c56607a68e6..1dc0f7a31e9a7 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/create_apm_query.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/create_apm_query.ts @@ -25,7 +25,9 @@ export function createApmQuery(options: { const opts = { filters: [] as any[], metric: ApmMetric.getMetricFields(), - types: ['stats', 'beats_stats'], + type: 'beats_stats', + metricset: 'stats', + dsDataset: 'beats.stats', ...(options ?? {}), }; @@ -38,6 +40,5 @@ export function createApmQuery(options: { }, }, }); - return createQuery(opts); } diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.ts index e99ce0da1ef10..77b5e44754999 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.ts @@ -6,12 +6,13 @@ */ import { LegacyRequest, Cluster } from '../../types'; -import { checkParam } from '../error_missing_required'; import { createApmQuery } from './create_apm_query'; import { ApmMetric } from '../metrics'; import { apmAggResponseHandler, apmUuidsAgg, apmAggFilterPath } from './_apm_stats'; import { getTimeOfLastEvent } from './_get_time_of_last_event'; import { ElasticsearchResponse } from '../../../common/types/es'; +import { getLegacyIndexPattern } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; export function handleResponse(clusterUuid: string, response: ElasticsearchResponse) { const { apmTotal, totalEvents, memRss, versions } = apmAggResponseHandler(response); @@ -32,24 +33,24 @@ export function handleResponse(clusterUuid: string, response: ElasticsearchRespo }; } -export function getApmsForClusters( - req: LegacyRequest, - apmIndexPattern: string, - clusters: Cluster[] -) { - checkParam(apmIndexPattern, 'apmIndexPattern in apms/getApmsForClusters'); - +export function getApmsForClusters(req: LegacyRequest, clusters: Cluster[], ccs?: string) { const start = req.payload.timeRange.min; const end = req.payload.timeRange.max; const config = req.server.config(); const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); const cgroup = config.get('monitoring.ui.container.apm.enabled'); + const indexPatterns = getLegacyIndexPattern({ + moduleType: 'beats', + ccs: ccs || req.payload.ccs, + config: Globals.app.config, + }); + return Promise.all( clusters.map(async (cluster) => { const clusterUuid = cluster.elasticsearch?.cluster?.id ?? cluster.cluster_uuid; const params = { - index: apmIndexPattern, + index: indexPatterns, size: 0, ignore_unavailable: true, filter_path: apmAggFilterPath, @@ -70,7 +71,7 @@ export function getApmsForClusters( getTimeOfLastEvent({ req, callWithRequest, - apmIndexPattern, + apmIndexPattern: indexPatterns, start, end, clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.ts b/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.ts index b013cd8234c40..6f56a95c8cbb9 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.ts +++ b/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.ts @@ -26,9 +26,12 @@ export function createBeatsQuery(options: { end?: number; }) { const opts = { + moduleType: 'beats', filters: [] as any[], metric: BeatsMetric.getMetricFields(), - types: ['stats', 'beats_stats'], + type: 'beats_stats', + metricset: 'stats', + dsDataset: 'beats.stats', ...(options ?? {}), }; diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.ts index 3a0720f7ca195..ae723fa99bbde 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { checkParam } from '../error_missing_required'; import { BeatsClusterMetric } from '../metrics'; import { createBeatsQuery } from './create_beats_query'; import { beatsAggFilterPath, beatsUuidsAgg, beatsAggResponseHandler } from './_beats_stats'; import type { ElasticsearchResponse } from '../../../common/types/es'; import { LegacyRequest, Cluster } from '../../types'; +import { getLegacyIndexPattern } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; export function handleResponse(clusterUuid: string, response: ElasticsearchResponse) { const { beatTotal, beatTypes, totalEvents, bytesSent } = beatsAggResponseHandler(response); @@ -31,23 +32,21 @@ export function handleResponse(clusterUuid: string, response: ElasticsearchRespo }; } -export function getBeatsForClusters( - req: LegacyRequest, - beatsIndexPattern: string, - clusters: Cluster[] -) { - checkParam(beatsIndexPattern, 'beatsIndexPattern in beats/getBeatsForClusters'); - +export function getBeatsForClusters(req: LegacyRequest, clusters: Cluster[], ccs: string) { const start = req.payload.timeRange.min; const end = req.payload.timeRange.max; const config = req.server.config(); const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); - + const indexPatterns = getLegacyIndexPattern({ + moduleType: 'beats', + ccs, + config: Globals.app.config, + }); return Promise.all( clusters.map(async (cluster) => { const clusterUuid = cluster.elasticsearch?.cluster?.id ?? cluster.cluster_uuid; const params = { - index: beatsIndexPattern, + index: indexPatterns, size: 0, ignore_unavailable: true, filter_path: beatsAggFilterPath, diff --git a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts index 820b1bf24c6a1..9ead8833dce46 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts @@ -10,15 +10,24 @@ import { checkParam } from '../error_missing_required'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../common/types/es'; import { LegacyRequest } from '../../types'; +import { getNewIndexPatterns } from './get_index_patterns'; +import { Globals } from '../../static_globals'; async function findSupportedBasicLicenseCluster( req: LegacyRequest, clusters: ElasticsearchModifiedSource[], - kbnIndexPattern: string, + ccs: string, kibanaUuid: string, serverLog: (message: string) => void ) { - checkParam(kbnIndexPattern, 'kbnIndexPattern in cluster/findSupportedBasicLicenseCluster'); + const dataset = 'stats'; + const moduleType = 'kibana'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType, + dataset, + ccs, + }); serverLog( `Detected all clusters in monitoring data have basic license. Checking for supported admin cluster UUID for Kibana ${kibanaUuid}.` @@ -28,7 +37,7 @@ async function findSupportedBasicLicenseCluster( const gte = req.payload.timeRange.min; const lte = req.payload.timeRange.max; const kibanaDataResult: ElasticsearchResponse = (await callWithRequest(req, 'search', { - index: kbnIndexPattern, + index: indexPatterns, size: 1, ignore_unavailable: true, filter_path: ['hits.hits._source.cluster_uuid', 'hits.hits._source.cluster.id'], @@ -41,7 +50,7 @@ async function findSupportedBasicLicenseCluster( bool: { should: [ { term: { type: 'kibana_stats' } }, - { term: { 'metricset.name': 'stats' } }, + { term: { 'data_stream.dataset': 'kibana.stats' } }, ], }, }, @@ -58,7 +67,6 @@ async function findSupportedBasicLicenseCluster( cluster.isSupported = true; } } - serverLog( `Found basic license admin cluster UUID for Monitoring UI support: ${supportedClusterUuid}.` ); @@ -80,9 +88,7 @@ async function findSupportedBasicLicenseCluster( * Non-Basic license clusters and any cluster in a single-cluster environment * are also flagged as supported in this method. */ -export function flagSupportedClusters(req: LegacyRequest, kbnIndexPattern: string) { - checkParam(kbnIndexPattern, 'kbnIndexPattern in cluster/flagSupportedClusters'); - +export function flagSupportedClusters(req: LegacyRequest, ccs: string) { const config = req.server.config(); const serverLog = (message: string) => req.getLogger('supported-clusters').debug(message); const flagAllSupported = (clusters: ElasticsearchModifiedSource[]) => { @@ -124,13 +130,7 @@ export function flagSupportedClusters(req: LegacyRequest, kbnIndexPattern: strin // if all linked are basic licenses if (linkedClusterCount === basicLicenseCount) { const kibanaUuid = config.get('server.uuid') as string; - return await findSupportedBasicLicenseCluster( - req, - clusters, - kbnIndexPattern, - kibanaUuid, - serverLog - ); + return await findSupportedBasicLicenseCluster(req, clusters, ccs, kibanaUuid, serverLog); } // if some non-basic licenses diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.ts index 8ed5578e574a0..108e88529bf7c 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.ts @@ -13,19 +13,31 @@ import { createQuery } from '../create_query'; import { ElasticsearchMetric } from '../metrics'; import { ElasticsearchResponse } from '../../../common/types/es'; import { LegacyRequest } from '../../types'; +import { getNewIndexPatterns } from './get_index_patterns'; +import { Globals } from '../../static_globals'; -export function getClusterLicense(req: LegacyRequest, esIndexPattern: string, clusterUuid: string) { - checkParam(esIndexPattern, 'esIndexPattern in getClusterLicense'); +// is this being used anywhere? not called within the app +export function getClusterLicense(req: LegacyRequest, clusterUuid: string) { + const dataset = 'cluster_stats'; + const moduleType = 'elasticsearch'; + const indexPattern = getNewIndexPatterns({ + config: Globals.app.config, + moduleType, + dataset, + ccs: req.payload.ccs, + }); const params = { - index: esIndexPattern, + index: indexPattern, size: 1, ignore_unavailable: true, filter_path: ['hits.hits._source.license'], body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ - type: 'cluster_stats', + type: dataset, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, clusterUuid, metric: ElasticsearchMetric.getMetricFields(), }), diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_stats.ts index c671765a44548..97eebd678a975 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_stats.ts @@ -14,11 +14,10 @@ import { getClustersStats } from './get_clusters_stats'; * This will fetch the cluster stats and cluster state as a single object for the cluster specified by the {@code req}. * * @param {Object} req The incoming user's request - * @param {String} esIndexPattern The Elasticsearch index pattern * @param {String} clusterUuid The requested cluster's UUID * @return {Promise} The object cluster response. */ -export function getClusterStats(req: LegacyRequest, esIndexPattern: string, clusterUuid: string) { +export function getClusterStats(req: LegacyRequest, clusterUuid: string) { if (!clusterUuid) { throw badRequest( i18n.translate('xpack.monitoring.clusterStats.uuidNotSpecifiedErrorMessage', { @@ -29,7 +28,7 @@ export function getClusterStats(req: LegacyRequest, esIndexPattern: string, clus } // passing clusterUuid so `get_clusters` will filter for single cluster - return getClustersStats(req, esIndexPattern, clusterUuid).then((clusters) => { + return getClustersStats(req, clusterUuid).then((clusters) => { if (!clusters || clusters.length === 0) { throw notFound( i18n.translate('xpack.monitoring.clusterStats.uuidNotFoundErrorMessage', { diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts index 6e28070cdbfaa..570d6fadfeb90 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts @@ -52,15 +52,7 @@ export async function getClustersFromRequest( codePaths, }: { clusterUuid: string; start: number; end: number; codePaths: string[] } ) { - const { - esIndexPattern, - kbnIndexPattern, - lsIndexPattern, - beatsIndexPattern, - apmIndexPattern, - enterpriseSearchIndexPattern, - filebeatIndexPattern, - } = indexPatterns; + const { filebeatIndexPattern } = indexPatterns; const config = req.server.config(); const isStandaloneCluster = clusterUuid === STANDALONE_CLUSTER_CLUSTER_UUID; @@ -71,18 +63,11 @@ export async function getClustersFromRequest( clusters.push(getStandaloneClusterDefinition()); } else { // get clusters with stats and cluster state - clusters = await getClustersStats(req, esIndexPattern, clusterUuid); + clusters = await getClustersStats(req, clusterUuid, '*'); } if (!clusterUuid && !isStandaloneCluster) { - const indexPatternsToCheckForNonClusters = [ - lsIndexPattern, - beatsIndexPattern, - apmIndexPattern, - enterpriseSearchIndexPattern, - ]; - - if (await hasStandaloneClusters(req, indexPatternsToCheckForNonClusters)) { + if (await hasStandaloneClusters(req, '*')) { clusters.push(getStandaloneClusterDefinition()); } } @@ -106,7 +91,7 @@ export async function getClustersFromRequest( // add ml jobs and alerts data const mlJobs = isInCodePath(codePaths, [CODE_PATH_ML]) - ? await getMlJobsForCluster(req, esIndexPattern, cluster) + ? await getMlJobsForCluster(req, cluster, '*') : null; if (mlJobs !== null) { cluster.ml = { jobs: mlJobs }; @@ -128,7 +113,7 @@ export async function getClustersFromRequest( } // update clusters with license check results - const getSupportedClusters = flagSupportedClusters(req, kbnIndexPattern); + const getSupportedClusters = flagSupportedClusters(req, '*'); clusters = await getSupportedClusters(clusters); // add alerts data @@ -184,7 +169,7 @@ export async function getClustersFromRequest( // add kibana data const kibanas = isInCodePath(codePaths, [CODE_PATH_KIBANA]) && !isStandaloneCluster - ? await getKibanasForClusters(req, kbnIndexPattern, clusters) + ? await getKibanasForClusters(req, clusters, '*') : []; // add the kibana data to each cluster kibanas.forEach((kibana) => { @@ -197,8 +182,8 @@ export async function getClustersFromRequest( // add logstash data if (isInCodePath(codePaths, [CODE_PATH_LOGSTASH])) { - const logstashes = await getLogstashForClusters(req, lsIndexPattern, clusters); - const pipelines = await getLogstashPipelineIds({ req, lsIndexPattern, clusterUuid, size: 1 }); + const logstashes = await getLogstashForClusters(req, clusters, '*'); + const pipelines = await getLogstashPipelineIds({ req, clusterUuid, size: 1, ccs: '*' }); logstashes.forEach((logstash) => { const clusterIndex = clusters.findIndex( (cluster) => @@ -214,7 +199,7 @@ export async function getClustersFromRequest( // add beats data const beatsByCluster = isInCodePath(codePaths, [CODE_PATH_BEATS]) - ? await getBeatsForClusters(req, beatsIndexPattern, clusters) + ? await getBeatsForClusters(req, clusters, '*') : []; beatsByCluster.forEach((beats) => { const clusterIndex = clusters.findIndex( @@ -226,7 +211,7 @@ export async function getClustersFromRequest( // add apm data const apmsByCluster = isInCodePath(codePaths, [CODE_PATH_APM]) - ? await getApmsForClusters(req, apmIndexPattern, clusters) + ? await getApmsForClusters(req, clusters, '*') : []; apmsByCluster.forEach((apm) => { const clusterIndex = clusters.findIndex( @@ -244,7 +229,7 @@ export async function getClustersFromRequest( // add Enterprise Search data const enterpriseSearchByCluster = isInCodePath(codePaths, [CODE_PATH_ENTERPRISE_SEARCH]) - ? await getEnterpriseSearchForClusters(req, enterpriseSearchIndexPattern, clusters) + ? await getEnterpriseSearchForClusters(req, clusters, '*') : []; enterpriseSearchByCluster.forEach((entSearch) => { const clusterIndex = clusters.findIndex( @@ -259,7 +244,7 @@ export async function getClustersFromRequest( }); // check ccr configuration - const isCcrEnabled = await checkCcrEnabled(req, esIndexPattern); + const isCcrEnabled = await checkCcrEnabled(req, '*'); const kibanaUuid = config.get('server.uuid')!; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.ts index d732b43bc203b..fab74ef1d979f 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.ts @@ -10,6 +10,8 @@ import { find } from 'lodash'; import { checkParam } from '../error_missing_required'; import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../common/types/es'; import { LegacyRequest } from '../../types'; +import { getNewIndexPatterns } from './get_index_patterns'; +import { Globals } from '../../static_globals'; /** * Augment the {@clusters} with their cluster state's from the {@code response}. @@ -46,13 +48,7 @@ export function handleResponse( * * If there is no cluster state available for any cluster, then it will be returned without any cluster state information. */ -export function getClustersState( - req: LegacyRequest, - esIndexPattern: string, - clusters: ElasticsearchModifiedSource[] -) { - checkParam(esIndexPattern, 'esIndexPattern in cluster/getClustersHealth'); - +export function getClustersState(req: LegacyRequest, clusters: ElasticsearchModifiedSource[]) { const clusterUuids = clusters .filter((cluster) => !cluster.cluster_state || !cluster.elasticsearch?.cluster?.stats?.state) .map((cluster) => cluster.cluster_uuid || cluster.elasticsearch?.cluster?.id); @@ -63,8 +59,14 @@ export function getClustersState( return Promise.resolve(clusters); } + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'elasticsearch', + ccs: req.payload.ccs, + }); + const params = { - index: esIndexPattern, + index: indexPatterns, size: clusterUuids.length, ignore_unavailable: true, filter_path: [ diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.test.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.test.js index a8f7423c45100..db4339a23e88c 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.test.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.test.js @@ -43,7 +43,7 @@ describe('handleClusterStats', () => { }, }; - const clusters = handleClusterStats(response, { log: () => undefined }); + const clusters = handleClusterStats(response); expect(clusters.length).toEqual(1); expect(clusters[0].ccs).toEqual('cluster_one'); diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts index a2201ca958e35..80fe5ce3806a6 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts @@ -16,21 +16,22 @@ import { parseCrossClusterPrefix } from '../../../common/ccs_utils'; import { getClustersState } from './get_clusters_state'; import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../common/types/es'; import { LegacyRequest } from '../../types'; +import { getNewIndexPatterns } from './get_index_patterns'; +import { Globals } from '../../static_globals'; /** * This will fetch the cluster stats and cluster state as a single object per cluster. * * @param {Object} req The incoming user's request - * @param {String} esIndexPattern The Elasticsearch index pattern * @param {String} clusterUuid (optional) If not undefined, getClusters will filter for a single cluster * @return {Promise} A promise containing an array of clusters. */ -export function getClustersStats(req: LegacyRequest, esIndexPattern: string, clusterUuid: string) { +export function getClustersStats(req: LegacyRequest, clusterUuid: string, ccs?: string) { return ( - fetchClusterStats(req, esIndexPattern, clusterUuid) + fetchClusterStats(req, clusterUuid, ccs) .then((response) => handleClusterStats(response)) // augment older documents (e.g., from 2.x - 5.4) with their cluster_state - .then((clusters) => getClustersState(req, esIndexPattern, clusters)) + .then((clusters) => getClustersState(req, clusters)) ); } @@ -38,12 +39,19 @@ export function getClustersStats(req: LegacyRequest, esIndexPattern: string, clu * Query cluster_stats for all the cluster data * * @param {Object} req (required) - server request - * @param {String} esIndexPattern (required) - index pattern to use in searching for cluster_stats data * @param {String} clusterUuid (optional) - if not undefined, getClusters filters for a single clusterUuid * @return {Promise} Object representing each cluster. */ -function fetchClusterStats(req: LegacyRequest, esIndexPattern: string, clusterUuid: string) { - checkParam(esIndexPattern, 'esIndexPattern in getClusters'); +function fetchClusterStats(req: LegacyRequest, clusterUuid: string, ccs?: string) { + const dataset = 'cluster_stats'; + const moduleType = 'elasticsearch'; + const indexPattern = getNewIndexPatterns({ + config: Globals.app.config, + moduleType, + dataset, + // this is will be either *, a request value, or null + ccs: ccs || req.payload.ccs, + }); const config = req.server.config(); // Get the params from the POST body for the request @@ -51,7 +59,7 @@ function fetchClusterStats(req: LegacyRequest, esIndexPattern: string, clusterUu const end = req.payload.timeRange.max; const metric = ElasticsearchMetric.getMetricFields(); const params = { - index: esIndexPattern, + index: indexPattern, size: config.get('monitoring.ui.max_bucket_size'), ignore_unavailable: true, filter_path: [ @@ -80,7 +88,15 @@ function fetchClusterStats(req: LegacyRequest, esIndexPattern: string, clusterUu 'hits.hits._source.cluster_settings.cluster.metadata.display_name', ], body: { - query: createQuery({ type: 'cluster_stats', start, end, metric, clusterUuid }), + query: createQuery({ + type: dataset, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, + start, + end, + metric, + clusterUuid, + }), collapse: { field: 'cluster_uuid', }, diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.test.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.test.ts new file mode 100644 index 0000000000000..015bdd4c65ccf --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.test.ts @@ -0,0 +1,102 @@ +/* + * 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 { MonitoringConfig } from '../..'; +import { getNewIndexPatterns } from './get_index_patterns'; + +const getConfigWithCcs = (ccsEnabled: boolean) => { + return { + ui: { + ccs: { + enabled: ccsEnabled, + }, + }, + } as MonitoringConfig; +}; + +describe('getNewIndexPatterns', () => { + beforeEach(() => { + jest.resetModules(); + }); + it('returns local elasticsearch index patterns when ccs is enabled (default true) and no ccs payload', () => { + const indexPatterns = getNewIndexPatterns({ + config: getConfigWithCcs(true), + moduleType: 'elasticsearch', + }); + expect(indexPatterns).toBe('.monitoring-es-*,metrics-elasticsearch.*-*'); + }); + it('returns ecs only elasticsearch index patterns when specifying ecsLegacyOnly: true', () => { + const indexPatterns = getNewIndexPatterns({ + config: getConfigWithCcs(true), + moduleType: 'elasticsearch', + ecsLegacyOnly: true, + }); + expect(indexPatterns).toBe('.monitoring-es-8-*,metrics-elasticsearch.*-*'); + }); + it('returns local kibana index patterns when ccs is enabled with no ccs payload', () => { + const indexPatterns = getNewIndexPatterns({ + config: getConfigWithCcs(true), + moduleType: 'kibana', + }); + expect(indexPatterns).toBe('.monitoring-kibana-*,metrics-kibana.*-*'); + }); + it('returns logstash index patterns when ccs is enabled and no ccs payload', () => { + const indexPatterns = getNewIndexPatterns({ + config: getConfigWithCcs(true), + moduleType: 'logstash', + }); + expect(indexPatterns).toBe('.monitoring-logstash-*,metrics-logstash.*-*'); + }); + it('returns beats index patterns when ccs is enabled and no ccs payload', () => { + const indexPatterns = getNewIndexPatterns({ + config: getConfigWithCcs(true), + moduleType: 'beats', + }); + expect(indexPatterns).toBe('.monitoring-beats-*,metrics-beats.*-*'); + }); + it('returns elasticsearch index patterns with dataset', () => { + const indexPatterns = getNewIndexPatterns({ + config: getConfigWithCcs(true), + moduleType: 'elasticsearch', + dataset: 'cluster_stats', + }); + expect(indexPatterns).toBe('.monitoring-es-*,metrics-elasticsearch.cluster_stats-*'); + }); + it('returns elasticsearch index patterns without ccs prefixes when ccs is disabled', () => { + const indexPatterns = getNewIndexPatterns({ + config: getConfigWithCcs(false), + moduleType: 'elasticsearch', + }); + expect(indexPatterns).toBe('.monitoring-es-*,metrics-elasticsearch.*-*'); + }); + it('returns elasticsearch index patterns without ccs prefixes when ccs is disabled but ccs request payload has a value', () => { + const indexPatterns = getNewIndexPatterns({ + config: getConfigWithCcs(false), + ccs: 'myccs', + moduleType: 'elasticsearch', + }); + expect(indexPatterns).toBe('.monitoring-es-*,metrics-elasticsearch.*-*'); + }); + it('returns elasticsearch index patterns with custom ccs prefixes when ccs is enabled and ccs request payload has a value', () => { + const indexPatterns = getNewIndexPatterns({ + config: getConfigWithCcs(true), + ccs: 'myccs', + moduleType: 'elasticsearch', + }); + expect(indexPatterns).toBe('myccs:.monitoring-es-*,myccs:metrics-elasticsearch.*-*'); + }); + it('returns elasticsearch index patterns with ccs prefixes and local index patterns when ccs is enabled and ccs request payload value is *', () => { + const indexPatterns = getNewIndexPatterns({ + config: getConfigWithCcs(true), + ccs: '*', + moduleType: 'elasticsearch', + }); + expect(indexPatterns).toBe( + '*:.monitoring-es-*,.monitoring-es-*,*:metrics-elasticsearch.*-*,metrics-elasticsearch.*-*' + ); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts index 5f7e55cf94c5a..fe9a319780db5 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts @@ -9,12 +9,17 @@ import { LegacyServer } from '../../types'; import { prefixIndexPattern } from '../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH, + INDEX_PATTERN_ELASTICSEARCH_ECS, INDEX_PATTERN_KIBANA, INDEX_PATTERN_LOGSTASH, INDEX_PATTERN_BEATS, INDEX_ALERTS, + DS_INDEX_PATTERN_TYPES, + DS_INDEX_PATTERN_METRICS, + INDEX_PATTERN_TYPES, INDEX_PATTERN_ENTERPRISE_SEARCH, } from '../../../common/constants'; +import { MonitoringConfig } from '../..'; export function getIndexPatterns( server: LegacyServer, @@ -44,10 +49,90 @@ export function getIndexPatterns( ...Object.keys(additionalPatterns).reduce((accum, varName) => { return { ...accum, - [varName]: prefixIndexPattern(config, additionalPatterns[varName], ccs, true), + [varName]: prefixIndexPattern(config, additionalPatterns[varName], ccs), }; }, {}), }; - return indexPatterns; } +// calling legacy index patterns those that are .monitoring +export function getLegacyIndexPattern({ + moduleType, + ecsLegacyOnly = false, + config, + ccs, +}: { + moduleType: INDEX_PATTERN_TYPES; + ecsLegacyOnly?: boolean; + config: MonitoringConfig; + ccs?: string; +}) { + let indexPattern = ''; + switch (moduleType) { + case 'elasticsearch': + // there may be cases where we only want the legacy ecs version index pattern (>=8.0) + indexPattern = ecsLegacyOnly ? INDEX_PATTERN_ELASTICSEARCH_ECS : INDEX_PATTERN_ELASTICSEARCH; + break; + case 'kibana': + indexPattern = INDEX_PATTERN_KIBANA; + break; + case 'logstash': + indexPattern = INDEX_PATTERN_LOGSTASH; + break; + case 'beats': + indexPattern = INDEX_PATTERN_BEATS; + break; + case 'enterprisesearch': + indexPattern = INDEX_PATTERN_ENTERPRISE_SEARCH; + break; + default: + throw new Error(`invalid module type to create index pattern: ${moduleType}`); + } + return prefixIndexPattern(config, indexPattern, ccs); +} + +export function getDsIndexPattern({ + type = DS_INDEX_PATTERN_METRICS, + moduleType, + dataset, + namespace = '*', + config, + ccs, +}: { + type?: string; + dataset?: string; + moduleType: INDEX_PATTERN_TYPES; + namespace?: string; + config: MonitoringConfig; + ccs?: string; +}): string { + let datasetsPattern = ''; + if (dataset) { + datasetsPattern = `${moduleType}.${dataset}`; + } else { + datasetsPattern = `${moduleType}.*`; + } + return prefixIndexPattern(config, `${type}-${datasetsPattern}-${namespace}`, ccs); +} + +export function getNewIndexPatterns({ + config, + moduleType, + type = DS_INDEX_PATTERN_METRICS, + dataset, + namespace = '*', + ccs, + ecsLegacyOnly, +}: { + config: MonitoringConfig; + moduleType: INDEX_PATTERN_TYPES; + type?: DS_INDEX_PATTERN_TYPES; + dataset?: string; + namespace?: string; + ccs?: string; + ecsLegacyOnly?: boolean; +}): string { + const legacyIndexPattern = getLegacyIndexPattern({ moduleType, ecsLegacyOnly, config, ccs }); + const dsIndexPattern = getDsIndexPattern({ type, moduleType, dataset, namespace, config, ccs }); + return `${legacyIndexPattern},${dsIndexPattern}`; +} diff --git a/x-pack/plugins/monitoring/server/lib/create_query.test.js b/x-pack/plugins/monitoring/server/lib/create_query.test.js deleted file mode 100644 index 60fa6faa79e44..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/create_query.test.js +++ /dev/null @@ -1,114 +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 { set } from '@elastic/safer-lodash-set'; -import { MissingRequiredError } from './error_missing_required'; -import { ElasticsearchMetric } from './metrics'; -import { createQuery } from './create_query'; - -let metric; - -describe('Create Query', () => { - beforeEach(() => { - metric = ElasticsearchMetric.getMetricFields(); - }); - - it('Allows UUID to not be passed', () => { - const options = { metric }; - const result = createQuery(options); - const expected = set({}, 'bool.filter', []); - expect(result).toEqual(expected); - }); - - it('Uses Elasticsearch timestamp field for start and end time range by default', () => { - const options = { - uuid: 'abc123', - start: '2016-03-01 10:00:00', - end: '2016-03-01 10:00:01', - metric, - }; - const result = createQuery(options); - let expected = {}; - expected = set(expected, 'bool.filter[0].term', { - 'source_node.uuid': 'abc123', - }); - expected = set(expected, 'bool.filter[1].range.timestamp', { - format: 'epoch_millis', - gte: 1456826400000, - lte: 1456826401000, - }); - expect(result).toEqual(expected); - }); - - it('Injects uuid and timestamp fields dynamically, based on metric', () => { - const options = { - uuid: 'abc123', - start: '2016-03-01 10:00:00', - end: '2016-03-01 10:00:01', - metric: { - uuidField: 'testUuidField', - timestampField: 'testTimestampField', - }, - }; - const result = createQuery(options); - let expected = set({}, 'bool.filter[0].term.testUuidField', 'abc123'); - expected = set(expected, 'bool.filter[1].range.testTimestampField', { - format: 'epoch_millis', - gte: 1456826400000, - lte: 1456826401000, - }); - expect(result).toEqual(expected); - }); - - it('Throws if missing metric.timestampField', () => { - function callCreateQuery() { - const options = {}; // missing metric object - return createQuery(options); - } - expect(callCreateQuery).toThrowError(MissingRequiredError); - }); - - it('Throws if given uuid but missing metric.uuidField', () => { - function callCreateQuery() { - const options = { uuid: 'abc123', metric }; - delete options.metric.uuidField; - return createQuery(options); - } - expect(callCreateQuery).toThrowError(MissingRequiredError); - }); - - // TODO: tests were not running and need to be updated to pass - it.skip('Uses `type` option to add type filter with minimal fields', () => { - const options = { type: 'test-type-yay', metric }; - const result = createQuery(options); - let expected = {}; - expected = set(expected, 'bool.filter[0].term', { type: 'test-type-yay' }); - expect(result).to.be.eql(expected); - }); - - it.skip('Uses `type` option to add type filter with all other option fields', () => { - const options = { - type: 'test-type-yay', - uuid: 'abc123', - start: '2016-03-01 10:00:00', - end: '2016-03-01 10:00:01', - metric, - }; - const result = createQuery(options); - let expected = {}; - expected = set(expected, 'bool.filter[0].term', { type: 'test-type-yay' }); - expected = set(expected, 'bool.filter[1].term', { - 'source_node.uuid': 'abc123', - }); - expected = set(expected, 'bool.filter[2].range.timestamp', { - format: 'epoch_millis', - gte: 1456826400000, - lte: 1456826401000, - }); - expect(result).to.be.eql(expected); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/create_query.test.ts b/x-pack/plugins/monitoring/server/lib/create_query.test.ts new file mode 100644 index 0000000000000..0e78889ca71fa --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/create_query.test.ts @@ -0,0 +1,231 @@ +/* + * 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 { MissingRequiredError } from './error_missing_required'; +import { ElasticsearchMetric } from './metrics'; +import { createQuery } from './create_query'; + +interface Metric { + uuidField?: string; + timestampField: string; +} +let metric: Metric; + +describe('Create Query', () => { + beforeEach(() => { + metric = ElasticsearchMetric.getMetricFields(); + }); + + it('Allows UUID to not be passed', () => { + const options = { metric, clusterUuid: 'cuid123' }; + expect(createQuery(options)).toEqual({ + bool: { filter: [{ term: { cluster_uuid: 'cuid123' } }] }, + }); + }); + + it('Uses Elasticsearch timestamp field for start and end time range by default', () => { + const options = { + clusterUuid: 'cuid123', + uuid: 'abc123', + start: 1456826400000, + end: 14568264010000, + metric, + }; + expect(createQuery(options)).toEqual({ + bool: { + filter: [ + { term: { cluster_uuid: 'cuid123' } }, + { term: { 'source_node.uuid': 'abc123' } }, + { + range: { + timestamp: { format: 'epoch_millis', gte: 1456826400000, lte: 14568264010000 }, + }, + }, + ], + }, + }); + }); + + it('Injects uuid and timestamp fields dynamically, based on metric', () => { + const options = { + clusterUuid: 'cuid123', + uuid: 'abc123', + start: 1456826400000, + end: 14568264010000, + metric: { + uuidField: 'testUuidField', + timestampField: 'testTimestampField', + }, + }; + expect(createQuery(options)).toEqual({ + bool: { + filter: [ + { term: { cluster_uuid: 'cuid123' } }, + { term: { testUuidField: 'abc123' } }, + { + range: { + testTimestampField: { + format: 'epoch_millis', + gte: 1456826400000, + lte: 14568264010000, + }, + }, + }, + ], + }, + }); + }); + + it('Throws if missing metric.timestampField', () => { + function callCreateQuery() { + const options = { clusterUuid: 'cuid123' }; // missing metric object + return createQuery(options); + } + expect(callCreateQuery).toThrowError(MissingRequiredError); + }); + + it('Throws if given uuid but missing metric.uuidField', () => { + function callCreateQuery() { + const options = { uuid: 'abc123', clusterUuid: 'cuid123', metric }; + delete options.metric.uuidField; + return createQuery(options); + } + expect(callCreateQuery).toThrowError(MissingRequiredError); + }); + + it('Uses `type` option to add type filter with minimal fields', () => { + const options = { type: 'cluster_stats', clusterUuid: 'cuid123', metric }; + expect(createQuery(options)).toEqual({ + bool: { + filter: [ + { bool: { should: [{ term: { type: 'cluster_stats' } }] } }, + { term: { cluster_uuid: 'cuid123' } }, + ], + }, + }); + }); + + it('Uses `type` option to add type filter with all other option fields and no data stream fields', () => { + const options = { + type: 'cluster_stats', + clusterUuid: 'cuid123', + uuid: 'abc123', + start: 1456826400000, + end: 14568264000000, + metric, + }; + expect(createQuery(options)).toEqual({ + bool: { + filter: [ + { bool: { should: [{ term: { type: 'cluster_stats' } }] } }, + { term: { cluster_uuid: 'cuid123' } }, + { term: { 'source_node.uuid': 'abc123' } }, + { + range: { + timestamp: { format: 'epoch_millis', gte: 1456826400000, lte: 14568264000000 }, + }, + }, + ], + }, + }); + }); + + it('Uses `dsType` option to add filter with all other option fields', () => { + const options = { + dsDataset: 'elasticsearch.cluster_stats', + clusterUuid: 'cuid123', + uuid: 'abc123', + start: 1456826400000, + end: 14568264000000, + metric, + }; + expect(createQuery(options)).toEqual({ + bool: { + filter: [ + { + bool: { + should: [{ term: { 'data_stream.dataset': 'elasticsearch.cluster_stats' } }], + }, + }, + { term: { cluster_uuid: 'cuid123' } }, + { term: { 'source_node.uuid': 'abc123' } }, + { + range: { + timestamp: { format: 'epoch_millis', gte: 1456826400000, lte: 14568264000000 }, + }, + }, + ], + }, + }); + }); + + it('Uses legacy `type`, `dsDataset`, `metricset` options to add type filters and data stream filters with minimal fields that defaults to `metrics` data_stream', () => { + const options = { + type: 'cluster_stats', + metricset: 'cluster_stats', + dsDataset: 'elasticsearch.cluster_stats', + clusterUuid: 'cuid123', + metric, + }; + expect(createQuery(options)).toEqual({ + bool: { + filter: [ + { + bool: { + should: [ + { + term: { + 'data_stream.dataset': 'elasticsearch.cluster_stats', + }, + }, + { + term: { + 'metricset.name': 'cluster_stats', + }, + }, + { term: { type: 'cluster_stats' } }, + ], + }, + }, + { term: { cluster_uuid: 'cuid123' } }, + ], + }, + }); + }); + + it('Uses legacy `type`, `metricset`, `dsDataset`, and `filters` options', () => { + const options = { + type: 'cluster_stats', + metricset: 'cluster_stats', + dsDataset: 'elasticsearch.cluster_stats', + clusterUuid: 'cuid123', + metric, + filters: [ + { + term: { 'source_node.uuid': `nuid123` }, + }, + ], + }; + expect(createQuery(options)).toEqual({ + bool: { + filter: [ + { + bool: { + should: [ + { term: { 'data_stream.dataset': 'elasticsearch.cluster_stats' } }, + { term: { 'metricset.name': 'cluster_stats' } }, + { term: { type: 'cluster_stats' } }, + ], + }, + }, + { term: { cluster_uuid: 'cuid123' } }, + { term: { 'source_node.uuid': 'nuid123' } }, + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/create_query.ts b/x-pack/plugins/monitoring/server/lib/create_query.ts index 8dead521d24fb..051b0ed6b4f9c 100644 --- a/x-pack/plugins/monitoring/server/lib/create_query.ts +++ b/x-pack/plugins/monitoring/server/lib/create_query.ts @@ -56,7 +56,9 @@ export function createTimeFilter(options: { * document UUIDs, start time and end time, and injecting additional filters. * * Options object: - * @param {String} options.type - `type` field value of the documents + * @param {string} options.type - `type` field value of the documents in legay .monitoring indices + * @param {string} options.dsDataset - `data_stream.dataset` field values of the documents + * @param {string} options.metricset - `metricset.name` field values of the documents * @param {Array} options.filters - additional filters to add to the `bool` section of the query. Default: [] * @param {string} options.clusterUuid - a UUID of the cluster. Required. * @param {string} options.uuid - a UUID of the metric to filter for, or `null` if UUID should not be part of the query @@ -64,30 +66,44 @@ export function createTimeFilter(options: { * @param {Date} options.end - numeric timestamp (optional) * @param {Metric} options.metric - Metric instance or metric fields object @see ElasticsearchMetric.getMetricFields */ -export function createQuery(options: { + +interface CreateQueryOptions { type?: string; - types?: string[]; + dsDataset?: string; + metricset?: string; filters?: any[]; clusterUuid: string; uuid?: string; start?: number; end?: number; metric?: { uuidField?: string; timestampField: string }; -}) { - const { type, types, clusterUuid, uuid, filters } = defaults(options, { filters: [] }); +} +export function createQuery(options: CreateQueryOptions) { + const { type, metricset, dsDataset, clusterUuid, uuid, filters } = defaults(options, { + filters: [], + }); const isFromStandaloneCluster = clusterUuid === STANDALONE_CLUSTER_CLUSTER_UUID; + const terms = []; let typeFilter: any; + + // data_stream.dataset matches agent integration data streams + if (dsDataset) { + terms.push({ term: { 'data_stream.dataset': dsDataset } }); + } + // metricset.name matches standalone beats + if (metricset) { + terms.push({ term: { 'metricset.name': metricset } }); + } + // type matches legacy data if (type) { - typeFilter = { bool: { should: [{ term: { type } }, { term: { 'metricset.name': type } }] } }; - } else if (types) { + terms.push({ term: { type } }); + } + if (terms.length) { typeFilter = { bool: { - should: [ - ...types.map((t) => ({ term: { type: t } })), - ...types.map((t) => ({ term: { 'metricset.name': t } })), - ], + should: [...terms], }, }; } diff --git a/x-pack/plugins/monitoring/server/lib/details/get_metrics.test.js b/x-pack/plugins/monitoring/server/lib/details/get_metrics.test.js index 80234ee369aee..5ba7fd1207448 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_metrics.test.js +++ b/x-pack/plugins/monitoring/server/lib/details/get_metrics.test.js @@ -16,6 +16,18 @@ import aggMetricsBuckets from './__fixtures__/agg_metrics_buckets'; const min = 1498968000000; // 2017-07-02T04:00:00.000Z const max = 1499054399999; // 2017-07-03T03:59:59.999Z +jest.mock('../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + function getMockReq(metricsBuckets = []) { const config = { get: sinon.stub(), @@ -59,27 +71,25 @@ function getMockReq(metricsBuckets = []) { }; } -const indexPattern = []; - describe('getMetrics and getSeries', () => { it('should return metrics with non-derivative metric', async () => { const req = getMockReq(nonDerivMetricsBuckets); const metricSet = ['node_cpu_utilization']; - const result = await getMetrics(req, indexPattern, metricSet); + const result = await getMetrics(req, 'elasticsearch', metricSet); expect(result).toMatchSnapshot(); }); it('should return metrics with derivative metric', async () => { const req = getMockReq(derivMetricsBuckets); const metricSet = ['cluster_search_request_rate']; - const result = await getMetrics(req, indexPattern, metricSet); + const result = await getMetrics(req, 'elasticsearch', metricSet); expect(result).toMatchSnapshot(); }); it('should return metrics with metric containing custom aggs', async () => { const req = getMockReq(aggMetricsBuckets); const metricSet = ['cluster_index_latency']; - const result = await getMetrics(req, indexPattern, metricSet); + const result = await getMetrics(req, 'elasticsearch', metricSet); expect(result).toMatchSnapshot(); }); @@ -91,14 +101,14 @@ describe('getMetrics and getSeries', () => { keys: ['index_mem_fixed_bit_set', 'index_mem_versions'], }, ]; - const result = await getMetrics(req, indexPattern, metricSet); + const result = await getMetrics(req, 'elasticsearch', metricSet); expect(result).toMatchSnapshot(); }); it('should return metrics with metric that uses default calculation', async () => { const req = getMockReq(nonDerivMetricsBuckets); const metricSet = ['kibana_max_response_times']; - const result = await getMetrics(req, indexPattern, metricSet); + const result = await getMetrics(req, 'elasticsearch', metricSet); expect(result).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts b/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts index a8a1117839dfe..efca0b84b0d65 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts +++ b/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts @@ -11,20 +11,21 @@ import { getSeries } from './get_series'; import { calculateTimeseriesInterval } from '../calculate_timeseries_interval'; import { getTimezone } from '../get_timezone'; import { LegacyRequest } from '../../types'; +import { INDEX_PATTERN_TYPES } from '../../../common/constants'; type Metric = string | { keys: string | string[]; name: string }; // TODO: Switch to an options object argument here export async function getMetrics( req: LegacyRequest, - indexPattern: string, + moduleType: INDEX_PATTERN_TYPES, metricSet: Metric[] = [], filters: Array> = [], metricOptions: Record = {}, numOfBuckets: number = 0, groupBy: string | Record | null = null ) { - checkParam(indexPattern, 'indexPattern in details/getMetrics'); + checkParam(moduleType, 'moduleType in details/getMetrics'); checkParam(metricSet, 'metricSet in details/getMetrics'); const config = req.server.config(); @@ -53,7 +54,7 @@ export async function getMetrics( return Promise.all( metricNames.map((metricName) => { - return getSeries(req, indexPattern, metricName, metricOptions, filters, groupBy, { + return getSeries(req, moduleType, metricName, metricOptions, filters, groupBy, { min, max, bucketSize, diff --git a/x-pack/plugins/monitoring/server/lib/details/get_series.ts b/x-pack/plugins/monitoring/server/lib/details/get_series.ts index 99652cbff3ffd..3a053b16aad7c 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_series.ts +++ b/x-pack/plugins/monitoring/server/lib/details/get_series.ts @@ -16,9 +16,12 @@ import { formatTimestampToDuration } from '../../../common'; import { NORMALIZED_DERIVATIVE_UNIT, CALCULATE_DURATION_UNTIL, + INDEX_PATTERN_TYPES, STANDALONE_CLUSTER_CLUSTER_UUID, } from '../../../common/constants'; import { formatUTCTimestampForTimezone } from '../format_timezone'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; type SeriesBucket = Bucket & { metric_mb_deriv?: { normalized_value: number } }; @@ -117,7 +120,7 @@ function createMetricAggs(metric: Metric) { async function fetchSeries( req: LegacyRequest, - indexPattern: string, + moduleType: INDEX_PATTERN_TYPES, metric: Metric, metricOptions: any, groupBy: string | Record | null, @@ -175,8 +178,14 @@ async function fetchSeries( }; } + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType, + ccs: req.payload.ccs, + }); + const params = { - index: indexPattern, + index: indexPatterns, size: 0, ignore_unavailable: true, body: { @@ -327,14 +336,15 @@ function handleSeries( * TODO: This should be expanded to accept multiple metrics in a single request to allow a single date histogram to be used. * * @param {Object} req The incoming user's request. - * @param {String} indexPattern The relevant index pattern (not just for Elasticsearch!). + * @param {String} moduleType The relevant module eg: elasticsearch, kibana, logstash. * @param {String} metricName The name of the metric being plotted. * @param {Array} filters Any filters that should be applied to the query. * @return {Promise} The object response containing the {@code timeRange}, {@code metric}, and {@code data}. */ + export async function getSeries( req: LegacyRequest, - indexPattern: string, + moduleType: INDEX_PATTERN_TYPES, metricName: string, metricOptions: Record, filters: Array>, @@ -346,7 +356,7 @@ export async function getSeries( timezone, }: { min: string | number; max: string | number; bucketSize: number; timezone: string } ) { - checkParam(indexPattern, 'indexPattern in details/getSeries'); + checkParam(moduleType, 'moduleType in details/getSeries'); const metric = metrics[metricName]; if (!metric) { @@ -354,7 +364,7 @@ export async function getSeries( } const response = await fetchSeries( req, - indexPattern, + moduleType, metric, metricOptions, groupBy, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts index 17dc48d0b237e..902e4bf784a09 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts @@ -14,9 +14,18 @@ import { ElasticsearchMetric } from '../metrics'; import { createQuery } from '../create_query'; import { ElasticsearchResponse } from '../../../common/types/es'; import { LegacyRequest } from '../../types'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; -export async function checkCcrEnabled(req: LegacyRequest, esIndexPattern: string) { - checkParam(esIndexPattern, 'esIndexPattern in checkCcrEnabled'); +export async function checkCcrEnabled(req: LegacyRequest, ccs: string) { + const dataset = 'cluster_stats'; + const moduleType = 'elasticsearch'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType, + dataset, + ccs, + }); const start = moment.utc(req.payload.timeRange.min).valueOf(); const end = moment.utc(req.payload.timeRange.max).valueOf(); @@ -25,12 +34,14 @@ export async function checkCcrEnabled(req: LegacyRequest, esIndexPattern: string const metricFields = ElasticsearchMetric.getMetricFields(); const params = { - index: esIndexPattern, + index: indexPatterns, size: 1, ignore_unavailable: true, body: { query: createQuery({ - type: 'cluster_stats', + type: dataset, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, start, end, clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.ts index 52c5b5b97f77b..7f6eea717be76 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.ts @@ -19,6 +19,8 @@ import { ElasticsearchResponseHit, } from '../../../common/types/es'; import { LegacyRequest } from '../../types'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; /** * Filter out shard activity that we do not care about. @@ -86,37 +88,63 @@ export function handleMbLastRecoveries(resp: ElasticsearchResponse, start: numbe return filtered; } -export async function getLastRecovery( - req: LegacyRequest, - esIndexPattern: string, - esIndexPatternEcs: string, - size: number -) { - checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getLastRecovery'); - +export async function getLastRecovery(req: LegacyRequest, size: number) { const start = req.payload.timeRange.min; const end = req.payload.timeRange.max; const clusterUuid = req.params.clusterUuid; const metric = ElasticsearchMetric.getMetricFields(); + + const dataset = 'index_recovery'; + const moduleType = 'elasticsearch'; + const indexPattern = getNewIndexPatterns({ + config: Globals.app.config, + moduleType, + dataset, + ccs: req.payload.ccs, + }); + const legacyParams = { - index: esIndexPattern, + index: indexPattern, size: 1, ignore_unavailable: true, body: { _source: ['index_recovery.shards'], sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, - query: createQuery({ type: 'index_recovery', start, end, clusterUuid, metric }), + query: createQuery({ + type: dataset, + metricset: dataset, + start, + end, + clusterUuid, + metric, + }), }, }; + + const indexPatternEcs = getNewIndexPatterns({ + config: Globals.app.config, + moduleType, + dataset, + ccs: req.payload.ccs, + ecsLegacyOnly: true, + }); const ecsParams = { - index: esIndexPatternEcs, + index: indexPatternEcs, size, ignore_unavailable: true, body: { _source: ['elasticsearch.index.recovery', '@timestamp'], sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, - query: createQuery({ type: 'index_recovery', start, end, clusterUuid, metric }), + query: createQuery({ + type: dataset, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, + start, + end, + clusterUuid, + metric, + }), aggs: { max_timestamp: { max: { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.ts index b0c82bdc0f502..f321326ee090a 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.ts @@ -15,6 +15,8 @@ import { ElasticsearchMetric } from '../metrics'; import { ML_SUPPORTED_LICENSES } from '../../../common/constants'; import { ElasticsearchResponse, ElasticsearchSource } from '../../../common/types/es'; import { LegacyRequest } from '../../types'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; /* * Get a listing of jobs along with some metric data to use for the listing @@ -37,17 +39,26 @@ export function handleResponse(response: ElasticsearchResponse) { export type MLJobs = ReturnType; -export function getMlJobs(req: LegacyRequest, esIndexPattern: string) { - checkParam(esIndexPattern, 'esIndexPattern in getMlJobs'); - +export function getMlJobs(req: LegacyRequest) { const config = req.server.config(); const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); const start = req.payload.timeRange.min; // no wrapping in moment :) const end = req.payload.timeRange.max; const clusterUuid = req.params.clusterUuid; const metric = ElasticsearchMetric.getMetricFields(); + + const dataset = 'ml_job'; + const type = 'job_stats'; + const moduleType = 'elasticsearch'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + dataset, + }); + const params = { - index: esIndexPattern, + index: indexPatterns, size: maxBucketSize, ignore_unavailable: true, filter_path: [ @@ -69,7 +80,15 @@ export function getMlJobs(req: LegacyRequest, esIndexPattern: string) { body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, collapse: { field: 'job_stats.job_id' }, - query: createQuery({ types: ['ml_job', 'job_stats'], start, end, clusterUuid, metric }), + query: createQuery({ + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, + start, + end, + clusterUuid, + metric, + }), }, }; @@ -81,11 +100,7 @@ export function getMlJobs(req: LegacyRequest, esIndexPattern: string) { * cardinality isn't guaranteed to be accurate is the issue * but it will be as long as the precision threshold is >= the actual value */ -export function getMlJobsForCluster( - req: LegacyRequest, - esIndexPattern: string, - cluster: ElasticsearchSource -) { +export function getMlJobsForCluster(req: LegacyRequest, cluster: ElasticsearchSource, ccs: string) { const license = cluster.license ?? cluster.elasticsearch?.cluster?.stats?.license ?? {}; if (license.status === 'active' && includes(ML_SUPPORTED_LICENSES, license.type)) { @@ -94,19 +109,37 @@ export function getMlJobsForCluster( const end = req.payload.timeRange.max; const clusterUuid = req.params.clusterUuid; const metric = ElasticsearchMetric.getMetricFields(); + + const type = 'job_stats'; + const dataset = 'ml_job'; + const moduleType = 'elasticsearch'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType, + dataset, + ccs, + }); + const params = { - index: esIndexPattern, + index: indexPatterns, size: 0, ignore_unavailable: true, filter_path: 'aggregations.jobs_count.value', body: { - query: createQuery({ types: ['ml_job', 'job_stats'], start, end, clusterUuid, metric }), + query: createQuery({ + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, + start, + end, + clusterUuid, + metric, + }), aggs: { jobs_count: { cardinality: { field: 'job_stats.job_id' } }, }, }, }; - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); return callWithRequest(req, 'search', params).then((response: ElasticsearchResponse) => { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.ts index 8a03027a93a56..388c3a364c77a 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.ts @@ -15,6 +15,8 @@ import { createQuery } from '../../create_query'; import { ElasticsearchMetric } from '../../metrics'; import { ElasticsearchResponse } from '../../../../common/types/es'; import { LegacyRequest } from '../../../types'; +import { getNewIndexPatterns } from '../../cluster/get_index_patterns'; +import { Globals } from '../../../static_globals'; export function handleResponse(shardStats: any, indexUuid: string) { return (response: ElasticsearchResponse) => { @@ -64,7 +66,6 @@ export function handleResponse(shardStats: any, indexUuid: string) { export function getIndexSummary( req: LegacyRequest, - esIndexPattern: string, shardStats: any, { clusterUuid, @@ -73,7 +74,15 @@ export function getIndexSummary( end, }: { clusterUuid: string; indexUuid: string; start: number; end: number } ) { - checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndexSummary'); + const dataset = 'index'; // data_stream.dataset + const type = 'index_stats'; // legacy + const moduleType = 'elasticsearch'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + dataset, + moduleType, + ccs: req.payload.ccs, + }); const metric = ElasticsearchMetric.getMetricFields(); const filters = [ @@ -87,13 +96,15 @@ export function getIndexSummary( }, ]; const params = { - index: esIndexPattern, + index: indexPatterns, size: 1, ignore_unavailable: true, body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ - types: ['index', 'index_stats'], + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, start, end, clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts index a43feb8fc84a3..85e49f463526a 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts @@ -19,6 +19,8 @@ import { calculateRate } from '../../calculate_rate'; import { getUnassignedShards } from '../shards'; import { ElasticsearchResponse } from '../../../../common/types/es'; import { LegacyRequest } from '../../../types'; +import { getNewIndexPatterns } from '../../cluster/get_index_patterns'; +import { Globals } from '../../../static_globals'; export function handleResponse( resp: ElasticsearchResponse, @@ -95,7 +97,7 @@ export function handleResponse( } export function buildGetIndicesQuery( - esIndexPattern: string, + req: LegacyRequest, clusterUuid: string, { start, @@ -113,9 +115,18 @@ export function buildGetIndicesQuery( }); } const metricFields = ElasticsearchMetric.getMetricFields(); + const dataset = 'index'; // data_stream.dataset + const type = 'index_stats'; // legacy + const moduleType = 'elasticsearch'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + dataset, + moduleType, + }); return { - index: esIndexPattern, + index: indexPatterns, size, ignore_unavailable: true, filter_path: [ @@ -145,7 +156,9 @@ export function buildGetIndicesQuery( ], body: { query: createQuery({ - types: ['index', 'index_stats'], + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, start, end, clusterUuid, @@ -167,17 +180,14 @@ export function buildGetIndicesQuery( export function getIndices( req: LegacyRequest, - esIndexPattern: string, showSystemIndices: boolean = false, shardStats: any ) { - checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndices'); - const { min: start, max: end } = req.payload.timeRange; const clusterUuid = req.params.clusterUuid; const config = req.server.config(); - const params = buildGetIndicesQuery(esIndexPattern, clusterUuid, { + const params = buildGetIndicesQuery(req, clusterUuid, { start, end, showSystemIndices, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts index aed6b40675e45..a422ccf95527c 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts @@ -24,6 +24,8 @@ import { ElasticsearchLegacySource, } from '../../../../common/types/es'; import { LegacyRequest } from '../../../types'; +import { getNewIndexPatterns } from '../../cluster/get_index_patterns'; +import { Globals } from '../../../static_globals'; export function handleResponse( clusterState: ElasticsearchSource['cluster_state'], @@ -100,7 +102,6 @@ export function handleResponse( export function getNodeSummary( req: LegacyRequest, - esIndexPattern: string, clusterState: ElasticsearchSource['cluster_state'], shardStats: any, { @@ -110,25 +111,40 @@ export function getNodeSummary( end, }: { clusterUuid: string; nodeUuid: string; start: number; end: number } ) { - checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getNodeSummary'); - - // Build up the Elasticsearch request const metric = ElasticsearchMetric.getMetricFields(); const filters = [ { term: { 'source_node.uuid': nodeUuid }, }, ]; + + const dataset = 'node_stats'; + const moduleType = 'elasticsearch'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + dataset, + moduleType, + }); + const params = { - index: esIndexPattern, + index: indexPatterns, size: 1, ignore_unavailable: true, body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, - query: createQuery({ type: 'node_stats', start, end, clusterUuid, metric, filters }), + query: createQuery({ + type: dataset, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, + start, + end, + clusterUuid, + metric, + filters, + }), }, }; - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); return callWithRequest(req, 'search', params).then( handleResponse(clusterState, shardStats, nodeUuid) diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.test.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.test.js index e6f04e3c649e5..738ea9ecc6a36 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.test.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.test.js @@ -7,6 +7,18 @@ import { getNodeIds } from './get_node_ids'; +jest.mock('../../../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + describe('getNodeIds', () => { it('should return a list of ids and uuids', async () => { const callWithRequest = jest.fn().mockReturnValue({ @@ -37,6 +49,9 @@ describe('getNodeIds', () => { }, }, server: { + config: () => ({ + get: () => true, + }), plugins: { elasticsearch: { getCluster: () => ({ diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.ts index 03524eebd12e8..58f4a0b8aca56 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.ts @@ -10,16 +10,26 @@ import { get } from 'lodash'; import { ElasticsearchMetric } from '../../../metrics'; import { createQuery } from '../../../create_query'; import { LegacyRequest, Bucket } from '../../../../types'; +import { getNewIndexPatterns } from '../../../cluster/get_index_patterns'; +import { Globals } from '../../../../static_globals'; export async function getNodeIds( req: LegacyRequest, - indexPattern: string, { clusterUuid }: { clusterUuid: string }, size: number ) { const start = moment.utc(req.payload.timeRange.min).valueOf(); const end = moment.utc(req.payload.timeRange.max).valueOf(); + const dataset = 'node_stats'; + const moduleType = 'elasticsearch'; + const indexPattern = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + dataset, + }); + const params = { index: indexPattern, size: 0, @@ -27,7 +37,9 @@ export async function getNodeIds( filter_path: ['aggregations.composite_data.buckets'], body: { query: createQuery({ - type: 'node_stats', + type: dataset, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, start, end, metric: ElasticsearchMetric.getMetricFields(), diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts index 0b5c0337e6c47..2bd7078fa00a4 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts @@ -6,7 +6,6 @@ */ import moment from 'moment'; -import { checkParam } from '../../../error_missing_required'; import { createQuery } from '../../../create_query'; import { calculateAuto } from '../../../calculate_auto'; import { ElasticsearchMetric } from '../../../metrics'; @@ -15,6 +14,8 @@ import { handleResponse } from './handle_response'; import { LISTING_METRICS_NAMES, LISTING_METRICS_PATHS } from './nodes_listing_metrics'; import { LegacyRequest } from '../../../../types'; import { ElasticsearchModifiedSource } from '../../../../../common/types/es'; +import { getNewIndexPatterns } from '../../../cluster/get_index_patterns'; +import { Globals } from '../../../../static_globals'; /* Run an aggregation on node_stats to get stat data for the selected time * range for all the active nodes. Every option is a key to a configuration @@ -35,13 +36,10 @@ import { ElasticsearchModifiedSource } from '../../../../../common/types/es'; */ export async function getNodes( req: LegacyRequest, - esIndexPattern: string, pageOfNodes: Array<{ uuid: string }>, clusterStats: ElasticsearchModifiedSource, nodesShardCount: { nodes: { [nodeId: string]: { shardCount: number } } } ) { - checkParam(esIndexPattern, 'esIndexPattern in getNodes'); - const start = moment.utc(req.payload.timeRange.min).valueOf(); const orgStart = start; const end = moment.utc(req.payload.timeRange.max).valueOf(); @@ -67,13 +65,24 @@ export async function getNodes( }, ]; + const dataset = 'node_stats'; + const moduleType = 'elasticsearch'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + dataset, + }); + const params = { - index: esIndexPattern, + index: indexPatterns, size: config.get('monitoring.ui.max_bucket_size'), ignore_unavailable: true, body: { query: createQuery({ - type: 'node_stats', + type: dataset, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, start, end, clusterUuid, @@ -112,7 +121,6 @@ export async function getNodes( ...LISTING_METRICS_PATHS, ], }; - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); const response = await callWithRequest(req, 'search', params); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.test.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.test.js index c7939027a0fb3..f96cb8e7a1853 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.test.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.test.js @@ -47,7 +47,6 @@ describe('getPaginatedNodes', () => { }), }, }; - const esIndexPattern = '.monitoring-es-*'; const clusterUuid = '1abc'; const metricSet = ['foo', 'bar']; const pagination = { index: 0, size: 10 }; @@ -74,7 +73,6 @@ describe('getPaginatedNodes', () => { it('should return a subset based on the pagination parameters', async () => { const nodes = await getPaginatedNodes( req, - esIndexPattern, { clusterUuid }, metricSet, pagination, @@ -94,7 +92,6 @@ describe('getPaginatedNodes', () => { it('should return a sorted subset', async () => { const nodes = await getPaginatedNodes( req, - esIndexPattern, { clusterUuid }, metricSet, pagination, @@ -111,17 +108,11 @@ describe('getPaginatedNodes', () => { }); }); - it('should return a filterd subset', async () => { - const nodes = await getPaginatedNodes( - req, - esIndexPattern, - { clusterUuid }, - metricSet, - pagination, - sort, - 'tw', - { clusterStats, nodesShardCount } - ); + it('should return a filtered subset', async () => { + const nodes = await getPaginatedNodes(req, { clusterUuid }, metricSet, pagination, sort, 'tw', { + clusterStats, + nodesShardCount, + }); expect(nodes).toEqual({ pageOfNodes: [{ name: 'two', uuid: 2, isOnline: false, shardCount: 5, foo: 12 }], totalNodeCount: 1, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.ts index 118140fe3f9cd..d353ea844cdf9 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.ts @@ -41,7 +41,6 @@ interface Node { export async function getPaginatedNodes( req: LegacyRequest, - esIndexPattern: string, { clusterUuid }: { clusterUuid: string }, metricSet: string[], pagination: { index: number; size: number }, @@ -59,7 +58,7 @@ export async function getPaginatedNodes( ) { const config = req.server.config(); const size = Number(config.get('monitoring.ui.max_bucket_size')); - const nodes: Node[] = await getNodeIds(req, esIndexPattern, { clusterUuid }, size); + const nodes: Node[] = await getNodeIds(req, { clusterUuid }, size); // Add `isOnline` and shards from the cluster state and shard stats const clusterState = clusterStats?.cluster_state ?? { nodes: {} }; @@ -87,13 +86,14 @@ export async function getPaginatedNodes( }; const metricSeriesData = await getMetrics( req, - esIndexPattern, + 'elasticsearch', metricSet, filters, { nodes }, 4, groupBy ); + for (const metricName in metricSeriesData) { if (!metricSeriesData.hasOwnProperty(metricName)) { continue; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.test.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.test.js index 6521f1f435cbc..6a9e01166ea87 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.test.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.test.js @@ -7,6 +7,18 @@ import { getIndicesUnassignedShardStats } from './get_indices_unassigned_shard_stats'; +jest.mock('../../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + describe('getIndicesUnassignedShardStats', () => { it('should return the unassigned shard stats for indices', async () => { const indices = { @@ -16,6 +28,7 @@ describe('getIndicesUnassignedShardStats', () => { }; const req = { + payload: {}, server: { config: () => ({ get: () => {}, @@ -52,9 +65,8 @@ describe('getIndicesUnassignedShardStats', () => { }, }, }; - const esIndexPattern = '*'; const cluster = {}; - const stats = await getIndicesUnassignedShardStats(req, esIndexPattern, cluster); + const stats = await getIndicesUnassignedShardStats(req, cluster); expect(stats.indices).toEqual(indices); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.ts index 87f79ff5b9b44..b0ba916b53eb8 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.ts @@ -16,12 +16,10 @@ import { ElasticsearchMetric } from '../../metrics'; import { calculateIndicesTotals } from './calculate_shard_stat_indices_totals'; import { LegacyRequest } from '../../../types'; import { ElasticsearchModifiedSource } from '../../../../common/types/es'; +import { getNewIndexPatterns } from '../../cluster/get_index_patterns'; +import { Globals } from '../../../static_globals'; -async function getUnassignedShardData( - req: LegacyRequest, - esIndexPattern: string, - cluster: ElasticsearchModifiedSource -) { +async function getUnassignedShardData(req: LegacyRequest, cluster: ElasticsearchModifiedSource) { const config = req.server.config(); const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); const metric = ElasticsearchMetric.getMetricFields(); @@ -38,14 +36,26 @@ async function getUnassignedShardData( }); } + const dataset = 'shard'; // data_stream.dataset + const type = 'shards'; // legacy + const moduleType = 'elasticsearch'; + const indexPattern = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + dataset, + }); + const params = { - index: esIndexPattern, + index: indexPattern, size: 0, ignore_unavailable: true, body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ - types: ['shard', 'shards'], + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, clusterUuid: cluster.cluster_uuid ?? cluster.elasticsearch?.cluster?.id, metric, filters, @@ -84,12 +94,9 @@ async function getUnassignedShardData( export async function getIndicesUnassignedShardStats( req: LegacyRequest, - esIndexPattern: string, cluster: ElasticsearchModifiedSource ) { - checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getShardStats'); - - const response = await getUnassignedShardData(req, esIndexPattern, cluster); + const response = await getUnassignedShardData(req, cluster); const indices = get(response, 'aggregations.indices.buckets', []).reduce( (accum: any, bucket: any) => { const index = bucket.key; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.test.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.test.js index 16d8693ca931d..c5c77ef389427 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.test.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.test.js @@ -7,6 +7,18 @@ import { getNodesShardCount } from './get_nodes_shard_count'; +jest.mock('../../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + describe('getNodeShardCount', () => { it('should return the shard count per node', async () => { const nodes = { @@ -16,6 +28,7 @@ describe('getNodeShardCount', () => { }; const req = { + payload: {}, server: { config: () => ({ get: () => {}, @@ -38,9 +51,8 @@ describe('getNodeShardCount', () => { }, }, }; - const esIndexPattern = '*'; const cluster = {}; - const counts = await getNodesShardCount(req, esIndexPattern, cluster); + const counts = await getNodesShardCount(req, cluster); expect(counts.nodes).toEqual(nodes); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.ts index 12ce144ebf5c5..a8ce0d429f06c 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.ts @@ -14,12 +14,10 @@ import { createQuery } from '../../create_query'; import { ElasticsearchMetric } from '../../metrics'; import { LegacyRequest } from '../../../types'; import { ElasticsearchModifiedSource } from '../../../../common/types/es'; +import { getNewIndexPatterns } from '../../cluster/get_index_patterns'; +import { Globals } from '../../../static_globals'; -async function getShardCountPerNode( - req: LegacyRequest, - esIndexPattern: string, - cluster: ElasticsearchModifiedSource -) { +async function getShardCountPerNode(req: LegacyRequest, cluster: ElasticsearchModifiedSource) { const config = req.server.config(); const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); const metric = ElasticsearchMetric.getMetricFields(); @@ -35,15 +33,26 @@ async function getShardCountPerNode( }, }); } + const dataset = 'shard'; // data_stream.dataset + const type = 'shards'; // legacy + const moduleType = 'elasticsearch'; + const indexPattern = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + dataset, + }); const params = { - index: esIndexPattern, + index: indexPattern, size: 0, ignore_unavailable: true, body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ - types: ['shard', 'shards'], + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, clusterUuid: cluster.cluster_uuid ?? cluster.elasticsearch?.cluster?.id, metric, filters, @@ -63,14 +72,8 @@ async function getShardCountPerNode( return await callWithRequest(req, 'search', params); } -export async function getNodesShardCount( - req: LegacyRequest, - esIndexPattern: string, - cluster: ElasticsearchModifiedSource -) { - checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getShardStats'); - - const response = await getShardCountPerNode(req, esIndexPattern, cluster); +export async function getNodesShardCount(req: LegacyRequest, cluster: ElasticsearchModifiedSource) { + const response = await getShardCountPerNode(req, cluster); const nodes = get(response, 'aggregations.nodes.buckets', []).reduce( (accum: any, bucket: any) => { accum[bucket.key] = { shardCount: bucket.doc_count }; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.ts index 26a1f88c719cf..cede4bf921419 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.ts @@ -6,13 +6,16 @@ */ // @ts-ignore -import { checkParam } from '../../error_missing_required'; +import { StringOptions } from '@kbn/config-schema/target_types/types'; // @ts-ignore import { createQuery } from '../../create_query'; // @ts-ignore import { ElasticsearchMetric } from '../../metrics'; import { ElasticsearchResponse, ElasticsearchLegacySource } from '../../../../common/types/es'; import { LegacyRequest } from '../../../types'; +import { getNewIndexPatterns } from '../../cluster/get_index_patterns'; +import { Globals } from '../../../static_globals'; + export function handleResponse(response: ElasticsearchResponse) { const hits = response.hits?.hits; if (!hits) { @@ -57,15 +60,12 @@ export function handleResponse(response: ElasticsearchResponse) { export function getShardAllocation( req: LegacyRequest, - esIndexPattern: string, { shardFilter, stateUuid, showSystemIndices = false, }: { shardFilter: any; stateUuid: string; showSystemIndices: boolean } ) { - checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getShardAllocation'); - const filters = [ { bool: { @@ -100,15 +100,32 @@ export function getShardAllocation( const config = req.server.config(); const clusterUuid = req.params.clusterUuid; const metric = ElasticsearchMetric.getMetricFields(); + + const dataset = 'shard'; // data_stream.dataset + const type = 'shards'; // legacy + const moduleType = 'elasticsearch'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + dataset, + moduleType, + }); + const params = { - index: esIndexPattern, + index: indexPatterns, size: config.get('monitoring.ui.max_bucket_size'), ignore_unavailable: true, body: { - query: createQuery({ types: ['shard', 'shards'], clusterUuid, metric, filters }), + query: createQuery({ + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, + clusterUuid, + metric, + filters, + }), }, }; - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); return callWithRequest(req, 'search', params).then(handleResponse); } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.ts index 0ee047b2b938b..29cbcb9ac0567 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.ts @@ -20,6 +20,8 @@ import { getShardAggs } from './get_shard_stat_aggs'; import { calculateIndicesTotals } from './calculate_shard_stat_indices_totals'; import { LegacyRequest } from '../../../types'; import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../../common/types/es'; +import { getNewIndexPatterns } from '../../cluster/get_index_patterns'; +import { Globals } from '../../../static_globals'; export function handleResponse( resp: ElasticsearchResponse, @@ -55,11 +57,18 @@ export function handleResponse( export function getShardStats( req: LegacyRequest, - esIndexPattern: string, cluster: ElasticsearchModifiedSource, { includeNodes = false, includeIndices = false, indexName = null, nodeUuid = null } = {} ) { - checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getShardStats'); + const dataset = 'shard'; // data_stream.dataset + const type = 'shards'; // legacy + const moduleType = 'elasticsearch'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + dataset, + }); const config = req.server.config(); const metric = ElasticsearchMetric.getMetricFields(); @@ -95,13 +104,15 @@ export function getShardStats( }); } const params = { - index: esIndexPattern, + index: indexPatterns, size: 0, ignore_unavailable: true, body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ - types: ['shard', 'shards'], + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, clusterUuid: cluster.cluster_uuid ?? cluster.elasticsearch?.cluster?.id, metric, filters, @@ -111,7 +122,6 @@ export function getShardStats( }, }, }; - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); return callWithRequest(req, 'search', params).then((resp) => { return handleResponse(resp, includeNodes, includeIndices, cluster); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/normalize_shard_objects.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/normalize_shard_objects.ts index 6aa84a0809e96..c6f38c7ac795f 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/normalize_shard_objects.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/normalize_shard_objects.ts @@ -48,7 +48,8 @@ export function normalizeNodeShards(masterNode: string) { [node.key]: { shardCount: node.doc_count, indexCount: node.index_count.value, - name: node.node_names.buckets[0].key, + // this field is always missing, problem with package? elasticsearch.node.name ECS field doesn't exist + name: node.node_names.buckets[0]?.key || 'NO NAME', node_ids: nodeIds, type: calculateNodeType(_node, masterNode), // put the "star" icon on the node link in the shard allocator }, diff --git a/x-pack/plugins/monitoring/server/lib/enterprise_search/create_enterprise_search_query.ts b/x-pack/plugins/monitoring/server/lib/enterprise_search/create_enterprise_search_query.ts index 3a418e010e1f7..5e5c972d2ba46 100644 --- a/x-pack/plugins/monitoring/server/lib/enterprise_search/create_enterprise_search_query.ts +++ b/x-pack/plugins/monitoring/server/lib/enterprise_search/create_enterprise_search_query.ts @@ -16,7 +16,6 @@ import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; */ export function createEnterpriseSearchQuery(options: { filters?: any[]; - types?: string[]; metric?: EnterpriseSearchMetricFields; uuid?: string; start?: number; @@ -25,7 +24,6 @@ export function createEnterpriseSearchQuery(options: { const opts = { filters: [] as any[], metric: EnterpriseSearchMetric.getMetricFields(), - types: ['health', 'stats'], clusterUuid: STANDALONE_CLUSTER_CLUSTER_UUID, // This is to disable the stack monitoring clusterUuid filter ...(options ?? {}), }; @@ -33,6 +31,26 @@ export function createEnterpriseSearchQuery(options: { opts.filters.push({ bool: { should: [ + { + term: { + type: 'health', + }, + }, + { + term: { + type: 'stats', + }, + }, + { + term: { + 'metricset.name': 'health', + }, + }, + { + term: { + 'metricset.name': 'stats', + }, + }, { term: { 'event.dataset': 'enterprisesearch.health' } }, { term: { 'event.dataset': 'enterprisesearch.stats' } }, ], diff --git a/x-pack/plugins/monitoring/server/lib/enterprise_search/get_enterprise_search_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/enterprise_search/get_enterprise_search_for_clusters.ts index 96ba1d18dc9e8..d46853fe48d3f 100644 --- a/x-pack/plugins/monitoring/server/lib/enterprise_search/get_enterprise_search_for_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/enterprise_search/get_enterprise_search_for_clusters.ts @@ -7,7 +7,6 @@ import { ElasticsearchResponse } from '../../../common/types/es'; import { LegacyRequest, Cluster } from '../../types'; -import { checkParam } from '../error_missing_required'; import { createEnterpriseSearchQuery } from './create_enterprise_search_query'; import { EnterpriseSearchMetric } from '../metrics'; import { @@ -15,6 +14,8 @@ import { entSearchAggResponseHandler, entSearchUuidsAgg, } from './_enterprise_search_stats'; +import { getLegacyIndexPattern } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; function handleResponse(clusterUuid: string, response: ElasticsearchResponse) { const stats = entSearchAggResponseHandler(response); @@ -27,24 +28,25 @@ function handleResponse(clusterUuid: string, response: ElasticsearchResponse) { export function getEnterpriseSearchForClusters( req: LegacyRequest, - entSearchIndexPattern: string, - clusters: Cluster[] + clusters: Cluster[], + ccs: string ) { - checkParam( - entSearchIndexPattern, - 'entSearchIndexPattern in enterprise_earch/getEnterpriseSearchForClusters' - ); - const start = req.payload.timeRange.min; const end = req.payload.timeRange.max; const config = req.server.config(); const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); + const indexPatterns = getLegacyIndexPattern({ + moduleType: 'enterprisesearch', + ccs, + config: Globals.app.config, + }); + return Promise.all( clusters.map(async (cluster) => { const clusterUuid = cluster.elasticsearch?.cluster?.id ?? cluster.cluster_uuid; const params = { - index: entSearchIndexPattern, + index: indexPatterns, size: 0, ignore_unavailable: true, filter_path: entSearchAggFilterPath, diff --git a/x-pack/plugins/monitoring/server/lib/enterprise_search/get_stats.ts b/x-pack/plugins/monitoring/server/lib/enterprise_search/get_stats.ts index bcb5617e0c9d8..73bd33daeac34 100644 --- a/x-pack/plugins/monitoring/server/lib/enterprise_search/get_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/enterprise_search/get_stats.ts @@ -8,28 +8,30 @@ import moment from 'moment'; import { ElasticsearchResponse } from '../../../common/types/es'; import { LegacyRequest } from '../../types'; -import { checkParam } from '../error_missing_required'; import { createEnterpriseSearchQuery } from './create_enterprise_search_query'; import { entSearchAggFilterPath, entSearchUuidsAgg, entSearchAggResponseHandler, } from './_enterprise_search_stats'; +import { getLegacyIndexPattern } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; -export async function getStats( - req: LegacyRequest, - entSearchIndexPattern: string, - clusterUuid: string -) { - checkParam(entSearchIndexPattern, 'entSearchIndexPattern in getStats'); - +export async function getStats(req: LegacyRequest, clusterUuid: string) { const config = req.server.config(); const start = moment.utc(req.payload.timeRange.min).valueOf(); const end = moment.utc(req.payload.timeRange.max).valueOf(); const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); + // just get the legacy pattern since no integration exists yet + const indexPattern = getLegacyIndexPattern({ + moduleType: 'enterprisesearch', + config: Globals.app.config, + ccs: req.payload.ccs, + }); + const params = { - index: entSearchIndexPattern, + index: indexPattern, filter_path: entSearchAggFilterPath, size: 0, ignore_unavailable: true, diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts index 81a653d370c0b..4116e9e5b86ac 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts @@ -12,6 +12,8 @@ import { checkParam, MissingRequiredError } from '../error_missing_required'; import { calculateAvailability } from '../calculate_availability'; import { LegacyRequest } from '../../types'; import { ElasticsearchResponse } from '../../../common/types/es'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; import { buildKibanaInfo } from './build_kibana_info'; export function handleResponse(resp: ElasticsearchResponse) { @@ -32,13 +34,16 @@ export function handleResponse(resp: ElasticsearchResponse) { export function getKibanaInfo( req: LegacyRequest, - kbnIndexPattern: string, { clusterUuid, kibanaUuid }: { clusterUuid: string; kibanaUuid: string } ) { - checkParam(kbnIndexPattern, 'kbnIndexPattern in getKibanaInfo'); - + const moduleType = 'kibana'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + }); const params = { - index: kbnIndexPattern, + index: indexPatterns, size: 1, ignore_unavailable: true, filter_path: [ diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.ts b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.ts index 6e55bf83bbd02..a476baa9c45d2 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.ts +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.ts @@ -15,6 +15,8 @@ import { calculateAvailability } from '../calculate_availability'; // @ts-ignore import { KibanaMetric } from '../metrics'; import { LegacyRequest } from '../../types'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; import { ElasticsearchResponse, ElasticsearchResponseHit } from '../../../common/types/es'; import { KibanaInfo, buildKibanaInfo } from './build_kibana_info'; @@ -52,24 +54,28 @@ interface Kibana { * - requests * - response times */ -export async function getKibanas( - req: LegacyRequest, - kbnIndexPattern: string, - { clusterUuid }: { clusterUuid: string } -) { - checkParam(kbnIndexPattern, 'kbnIndexPattern in getKibanas'); - +export async function getKibanas(req: LegacyRequest, { clusterUuid }: { clusterUuid: string }) { const config = req.server.config(); const start = moment.utc(req.payload.timeRange.min).valueOf(); const end = moment.utc(req.payload.timeRange.max).valueOf(); - + const moduleType = 'kibana'; + const type = 'kibana_stats'; + const dataset = 'stats'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + dataset, + }); const params = { - index: kbnIndexPattern, + index: indexPatterns, size: config.get('monitoring.ui.max_bucket_size'), ignore_unavailable: true, body: { query: createQuery({ - types: ['kibana_stats', 'stats'], + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, start, end, clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts index 5326976ec99ac..883ca17c98c5b 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts @@ -7,9 +7,10 @@ import { chain, find } from 'lodash'; import { LegacyRequest, Cluster, Bucket } from '../../types'; -import { checkParam } from '../error_missing_required'; import { createQuery } from '../create_query'; import { KibanaClusterMetric } from '../metrics'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; /* * Get high-level info for Kibanas in a set of clusters @@ -24,28 +25,34 @@ import { KibanaClusterMetric } from '../metrics'; * - number of instances * - combined health */ -export function getKibanasForClusters( - req: LegacyRequest, - kbnIndexPattern: string, - clusters: Cluster[] -) { - checkParam(kbnIndexPattern, 'kbnIndexPattern in kibana/getKibanasForClusters'); - +export function getKibanasForClusters(req: LegacyRequest, clusters: Cluster[], ccs: string) { const config = req.server.config(); const start = req.payload.timeRange.min; const end = req.payload.timeRange.max; + const moduleType = 'kibana'; + const type = 'kibana_stats'; + const dataset = 'stats'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType, + dataset, + ccs, + }); + return Promise.all( clusters.map((cluster) => { const clusterUuid = cluster.elasticsearch?.cluster?.id ?? cluster.cluster_uuid; const metric = KibanaClusterMetric.getMetricFields(); const params = { - index: kbnIndexPattern, + index: indexPatterns, size: 0, ignore_unavailable: true, body: { query: createQuery({ - types: ['stats', 'kibana_stats'], + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, start, end, clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts b/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts index 727e47b62bc92..72a794db76183 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts +++ b/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts @@ -13,7 +13,7 @@ import { InfraPluginSetup } from '../../../../infra/server'; export const initInfraSource = (config: MonitoringConfig, infraPlugin: InfraPluginSetup) => { if (infraPlugin) { - const filebeatIndexPattern = prefixIndexPattern(config, config.ui.logs.index, '*', true); + const filebeatIndexPattern = prefixIndexPattern(config, config.ui.logs.index, '*'); infraPlugin.defineInternalSourceConfiguration(INFRA_SOURCE_ID, { name: 'Elastic Stack Logs', logIndices: { diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_cluster_status.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_cluster_status.ts index dfd1eaa155069..308a750f6ef02 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_cluster_status.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_cluster_status.ts @@ -6,7 +6,6 @@ */ import { get } from 'lodash'; -import { checkParam } from '../error_missing_required'; import { getLogstashForClusters } from './get_logstash_for_clusters'; import { LegacyRequest } from '../../types'; @@ -16,18 +15,12 @@ import { LegacyRequest } from '../../types'; * Shared functionality between the different routes. * * @param {Object} req The incoming request. - * @param {String} lsIndexPattern The Logstash pattern to query for the current time range. * @param {String} clusterUuid The cluster UUID for the associated Elasticsearch cluster. * @returns {Promise} The cluster status object. */ -export function getClusterStatus( - req: LegacyRequest, - lsIndexPattern: string, - { clusterUuid }: { clusterUuid: string } -) { - checkParam(lsIndexPattern, 'lsIndexPattern in logstash/getClusterStatus'); +export function getClusterStatus(req: LegacyRequest, { clusterUuid }: { clusterUuid: string }) { const clusters = [{ cluster_uuid: clusterUuid }]; - return getLogstashForClusters(req, lsIndexPattern, clusters).then((clusterStatus) => + return getLogstashForClusters(req, clusters).then((clusterStatus) => get(clusterStatus, '[0].stats') ); } diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts index 20e611a5ee3da..a0be8efe5ebdf 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts @@ -8,9 +8,10 @@ import { get } from 'lodash'; import { LegacyRequest, Cluster, Bucket } from '../../types'; import { LOGSTASH } from '../../../common/constants'; -import { checkParam } from '../error_missing_required'; import { createQuery } from '../create_query'; import { LogstashClusterMetric } from '../metrics'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; const { MEMORY, PERSISTED } = LOGSTASH.QUEUE_TYPES; @@ -38,25 +39,35 @@ const getQueueTypes = (queueBuckets: Array + clusters: Array<{ cluster_uuid: string } | Cluster>, + ccs?: string ) { - checkParam(lsIndexPattern, 'lsIndexPattern in logstash/getLogstashForClusters'); - const start = req.payload.timeRange.min; const end = req.payload.timeRange.max; const config = req.server.config(); + const dataset = 'node_stats'; + const type = 'logstash_stats'; + const moduleType = 'logstash'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: ccs || req.payload.ccs, + moduleType, + dataset, + }); + return Promise.all( clusters.map((cluster) => { const clusterUuid = get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid); const params = { - index: lsIndexPattern, + index: indexPatterns, size: 0, ignore_unavailable: true, body: { query: createQuery({ - types: ['node_stats', 'logstash_stats'], + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, start, end, clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.ts index cee6c144c866e..a43eb9a7cd09f 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.ts @@ -18,6 +18,18 @@ interface HitParams { value?: string; } +jest.mock('../../static_globals', () => ({ + Globals: { + app: { + config: { + ui: { + ccs: { enabled: true }, + }, + }, + }, + }, +})); + // deletes, adds, or updates the properties based on a default object function createResponseObjHit(params?: HitParams[]): ElasticsearchResponseHit { const defaultResponseObj: ElasticsearchResponseHit = { @@ -189,7 +201,11 @@ describe('get_logstash_info', () => { then: jest.fn(), }); const req = { + payload: {}, server: { + config: () => ({ + get: () => undefined, + }), plugins: { elasticsearch: { getCluster: () => ({ @@ -199,7 +215,8 @@ describe('get_logstash_info', () => { }, }, } as unknown as LegacyRequest; - await getNodeInfo(req, '.monitoring-logstash-*', { + + await getNodeInfo(req, { clusterUuid: STANDALONE_CLUSTER_CLUSTER_UUID, logstashUuid: 'logstash_uuid', }); diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts index ebd1128dce364..92e2a836e08ff 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts @@ -6,12 +6,14 @@ */ import { merge } from 'lodash'; -import { checkParam, MissingRequiredError } from '../error_missing_required'; +import { MissingRequiredError } from '../error_missing_required'; import { calculateAvailability } from '../calculate_availability'; import { LegacyRequest } from '../../types'; import { ElasticsearchResponse } from '../../../common/types/es'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; import { standaloneClusterFilter } from '../standalone_clusters/standalone_cluster_query_filter'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; export function handleResponse(resp: ElasticsearchResponse) { const legacyStats = resp.hits?.hits[0]?._source?.logstash_stats; @@ -33,18 +35,25 @@ export function handleResponse(resp: ElasticsearchResponse) { export function getNodeInfo( req: LegacyRequest, - lsIndexPattern: string, { clusterUuid, logstashUuid }: { clusterUuid: string; logstashUuid: string } ) { - checkParam(lsIndexPattern, 'lsIndexPattern in getNodeInfo'); const isStandaloneCluster = clusterUuid === STANDALONE_CLUSTER_CLUSTER_UUID; const clusterFilter = isStandaloneCluster ? standaloneClusterFilter : { term: { cluster_uuid: clusterUuid } }; + const dataset = 'node_stats'; + const moduleType = 'logstash'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + dataset, + }); + const params = { - index: lsIndexPattern, + index: indexPatterns, size: 1, ignore_unavailable: true, filter_path: [ diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.ts index a25b57ab445e3..d7c490b1d2fd6 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.ts @@ -6,12 +6,13 @@ */ import moment from 'moment'; -import { checkParam } from '../error_missing_required'; import { createQuery } from '../create_query'; import { calculateAvailability } from '../calculate_availability'; import { LogstashMetric } from '../metrics'; import { LegacyRequest } from '../../types'; import { ElasticsearchResponse } from '../../../common/types/es'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; interface Logstash { jvm?: { @@ -64,28 +65,35 @@ interface Logstash { * - events * - config reloads */ -export async function getNodes( - req: LegacyRequest, - lsIndexPattern: string, - { clusterUuid }: { clusterUuid: string } -) { - checkParam(lsIndexPattern, 'lsIndexPattern in getNodes'); +export async function getNodes(req: LegacyRequest, { clusterUuid }: { clusterUuid: string }) { + const dataset = 'node_stats'; + const type = 'logstash_stats'; + const moduleType = 'logstash'; + + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + dataset, + }); const config = req.server.config(); const start = moment.utc(req.payload.timeRange.min).valueOf(); const end = moment.utc(req.payload.timeRange.max).valueOf(); const params = { - index: lsIndexPattern, + index: indexPatterns, size: config.get('monitoring.ui.max_bucket_size'), // FIXME ignore_unavailable: true, body: { query: createQuery({ + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, start, end, clusterUuid, metric: LogstashMetric.getMetricFields(), - types: ['node_stats', 'logstash_stats'], }), collapse: { field: 'logstash_stats.logstash.uuid', diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.ts index f8f993874a181..42c6a684234c4 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.ts @@ -42,7 +42,6 @@ import { interface GetPaginatedPipelinesParams { req: LegacyRequest; - lsIndexPattern: string; clusterUuid: string; logstashUuid?: string; metrics: { @@ -55,7 +54,6 @@ interface GetPaginatedPipelinesParams { } export async function getPaginatedPipelines({ req, - lsIndexPattern, clusterUuid, logstashUuid, metrics, @@ -70,16 +68,15 @@ export async function getPaginatedPipelines({ const size = config.get('monitoring.ui.max_bucket_size') as unknown as number; let pipelines = await getLogstashPipelineIds({ req, - lsIndexPattern, clusterUuid, logstashUuid, size, }); // this is needed for sorting if (sortField === throughputMetric) { - pipelines = await getPaginatedThroughputData(pipelines, req, lsIndexPattern, throughputMetric); + pipelines = await getPaginatedThroughputData(pipelines, req, throughputMetric); } else if (sortField === nodesCountMetric) { - pipelines = await getPaginatedNodesData(pipelines, req, lsIndexPattern, nodesCountMetric); + pipelines = await getPaginatedNodesData(pipelines, req, nodesCountMetric); } const filteredPipelines = filter(pipelines, queryText, ['id']); // We only support filtering by id right now @@ -91,7 +88,6 @@ export async function getPaginatedPipelines({ const response = { pipelines: await getPipelines({ req, - lsIndexPattern, pipelines: pageOfPipelines, throughputMetric, nodesCountMetric, @@ -131,9 +127,10 @@ function processPipelinesAPIResponse( async function getPaginatedThroughputData( pipelines: Pipeline[], req: LegacyRequest, - lsIndexPattern: string, throughputMetric: PipelineThroughputMetricKey ): Promise { + const dataset = 'node_stats'; + const moduleType = 'logstash'; const metricSeriesData: any = Object.values( await Promise.all( pipelines.map((pipeline) => { @@ -141,22 +138,19 @@ async function getPaginatedThroughputData( try { const data = await getMetrics( req, - lsIndexPattern, + moduleType, [throughputMetric], [ { bool: { should: [ + { term: { 'data_stream.dataset': `${moduleType}.${dataset}` } }, + { term: { 'metricset.name': dataset } }, { term: { type: 'logstash_stats', }, }, - { - term: { - 'metricset.name': 'node_stats', - }, - }, ], }, }, @@ -197,20 +191,22 @@ async function getPaginatedThroughputData( async function getPaginatedNodesData( pipelines: Pipeline[], req: LegacyRequest, - lsIndexPattern: string, nodesCountMetric: PipelineNodeCountMetricKey ): Promise { + const dataset = 'node_stats'; + const moduleType = 'logstash'; const pipelineWithMetrics = cloneDeep(pipelines); const metricSeriesData = await getMetrics( req, - lsIndexPattern, + moduleType, [nodesCountMetric], [ { bool: { should: [ + { term: { 'data_stream.dataset': `${moduleType}.${dataset}` } }, + { term: { 'metricset.name': dataset } }, { term: { type: 'logstash_stats' } }, - { term: { 'metricset.name': 'node_stats' } }, ], }, }, @@ -231,29 +227,17 @@ async function getPaginatedNodesData( async function getPipelines({ req, - lsIndexPattern, pipelines, throughputMetric, nodesCountMetric, }: { req: LegacyRequest; - lsIndexPattern: string; pipelines: Pipeline[]; throughputMetric: PipelineThroughputMetricKey; nodesCountMetric: PipelineNodeCountMetricKey; }): Promise { - const throughputPipelines = await getThroughputPipelines( - req, - lsIndexPattern, - pipelines, - throughputMetric - ); - const nodeCountPipelines = await getNodePipelines( - req, - lsIndexPattern, - pipelines, - nodesCountMetric - ); + const throughputPipelines = await getThroughputPipelines(req, pipelines, throughputMetric); + const nodeCountPipelines = await getNodePipelines(req, pipelines, nodesCountMetric); const finalPipelines = pipelines.map(({ id }) => { const matchThroughputPipeline = throughputPipelines.find((p) => p.id === id); const matchNodesCountPipeline = nodeCountPipelines.find((p) => p.id === id); @@ -276,32 +260,30 @@ async function getPipelines({ async function getThroughputPipelines( req: LegacyRequest, - lsIndexPattern: string, pipelines: Pipeline[], throughputMetric: string ): Promise { + const dataset = 'node_stats'; + const moduleType = 'logstash'; const metricsResponse = await Promise.all( pipelines.map((pipeline) => { return new Promise(async (resolve, reject) => { try { const data = await getMetrics( req, - lsIndexPattern, + moduleType, [throughputMetric], [ { bool: { should: [ + { term: { 'data_stream.dataset': `${moduleType}.${dataset}` } }, + { term: { 'metricset.name': dataset } }, { term: { type: 'logstash_stats', }, }, - { - term: { - 'metricset.name': 'node_stats', - }, - }, ], }, }, @@ -322,20 +304,22 @@ async function getThroughputPipelines( async function getNodePipelines( req: LegacyRequest, - lsIndexPattern: string, pipelines: Pipeline[], nodesCountMetric: string ): Promise { + const moduleType = 'logstash'; + const dataset = 'node_stats'; const metricData = await getMetrics( req, - lsIndexPattern, + moduleType, [nodesCountMetric], [ { bool: { should: [ + { term: { 'data_stream.dataset': `${moduleType}.${dataset}` } }, + { term: { 'metricset.name': dataset } }, { term: { type: 'logstash_stats' } }, - { term: { 'metricset.name': 'node_stats' } }, ], }, }, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts index 1e8eb94df4836..16a96b132483f 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts @@ -7,7 +7,6 @@ import boom from '@hapi/boom'; import { get } from 'lodash'; -import { checkParam } from '../error_missing_required'; import { getPipelineStateDocument } from './get_pipeline_state_document'; import { getPipelineStatsAggregation } from './get_pipeline_stats_aggregation'; import { calculateTimeseriesInterval } from '../calculate_timeseries_interval'; @@ -122,13 +121,10 @@ export function _enrichStateWithStatsAggregation( export async function getPipeline( req: LegacyRequest, config: { get: (key: string) => string | undefined }, - lsIndexPattern: string, clusterUuid: string, pipelineId: string, version: PipelineVersion ) { - checkParam(lsIndexPattern, 'lsIndexPattern in getPipeline'); - // Determine metrics' timeseries interval based on version's timespan const minIntervalSeconds = Math.max(Number(config.get('monitoring.ui.min_interval_seconds')), 30); const timeseriesInterval = calculateTimeseriesInterval( @@ -140,14 +136,12 @@ export async function getPipeline( const [stateDocument, statsAggregation] = await Promise.all([ getPipelineStateDocument({ req, - logstashIndexPattern: lsIndexPattern, clusterUuid, pipelineId, version, }), getPipelineStatsAggregation({ req, - logstashIndexPattern: lsIndexPattern, timeseriesInterval, clusterUuid, pipelineId, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts index c9b7a3adfc18e..7654ed551b63b 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts @@ -10,20 +10,22 @@ import { get } from 'lodash'; import { LegacyRequest, Bucket, Pipeline } from '../../types'; import { createQuery } from '../create_query'; import { LogstashMetric } from '../metrics'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; interface GetLogstashPipelineIdsParams { req: LegacyRequest; - lsIndexPattern: string; clusterUuid: string; size: number; logstashUuid?: string; + ccs?: string; } export async function getLogstashPipelineIds({ req, - lsIndexPattern, clusterUuid, logstashUuid, size, + ccs, }: GetLogstashPipelineIdsParams): Promise { const start = moment.utc(req.payload.timeRange.min).valueOf(); const end = moment.utc(req.payload.timeRange.max).valueOf(); @@ -33,8 +35,17 @@ export async function getLogstashPipelineIds({ filters.push({ term: { 'logstash_stats.logstash.uuid': logstashUuid } }); } + const dataset = 'node_stats'; + const moduleType = 'logstash'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: ccs || req.payload.ccs, + moduleType, + dataset, + }); + const params = { - index: lsIndexPattern, + index: indexPatterns, size: 0, ignore_unavailable: true, filter_path: ['aggregations.nest.id.buckets', 'aggregations.nest_mb.id.buckets'], diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts index f62d4de1219ec..6c2504efe29ff 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts @@ -9,20 +9,29 @@ import { createQuery } from '../create_query'; import { LogstashMetric } from '../metrics'; import { LegacyRequest, PipelineVersion } from '../../types'; import { ElasticsearchResponse } from '../../../common/types/es'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; export async function getPipelineStateDocument({ req, - logstashIndexPattern, clusterUuid, pipelineId, version, }: { req: LegacyRequest; - logstashIndexPattern: string; clusterUuid: string; pipelineId: string; version: PipelineVersion; }) { + const dataset = 'node'; + const type = 'logstash_state'; + const moduleType = 'logstash'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + dataset, + }); const { callWithRequest } = req.server.plugins?.elasticsearch.getCluster('monitoring'); const filters = [ { term: { 'logstash_state.pipeline.id': pipelineId } }, @@ -35,14 +44,16 @@ export async function getPipelineStateDocument({ // This is important because a user may pick a very narrow time picker window. If we were to use a start/end value // that could result in us being unable to render the graph // Use the logstash_stats documents to determine whether the instance is up/down - types: ['logstash_state', 'node'], + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, metric: LogstashMetric.getMetricFields(), clusterUuid, filters, }); const params = { - index: logstashIndexPattern, + index: indexPatterns, size: 1, ignore_unavailable: true, body: { diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.ts index 70f856b25f8c8..31cfa4b4a0291 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.ts @@ -6,8 +6,10 @@ */ import { LegacyRequest, PipelineVersion } from '../../types'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; import { createQuery } from '../create_query'; import { LogstashMetric } from '../metrics'; +import { Globals } from '../../static_globals'; function scalarCounterAggregation( field: string, @@ -111,16 +113,23 @@ function createScopedAgg(pipelineId: string, pipelineHash: string, maxBucketSize function fetchPipelineLatestStats( query: object, - logstashIndexPattern: string, pipelineId: string, version: PipelineVersion, maxBucketSize: string, callWithRequest: any, req: LegacyRequest ) { + const dataset = 'node_stats'; + const moduleType = 'logstash'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + dataset, + }); const pipelineAggregation = createScopedAgg(pipelineId, version.hash, maxBucketSize); const params = { - index: logstashIndexPattern, + index: indexPatterns, size: 0, ignore_unavailable: true, filter_path: [ @@ -149,14 +158,12 @@ function fetchPipelineLatestStats( export function getPipelineStatsAggregation({ req, - logstashIndexPattern, timeseriesInterval, clusterUuid, pipelineId, version, }: { req: LegacyRequest; - logstashIndexPattern: string; timeseriesInterval: number; clusterUuid: string; pipelineId: string; @@ -197,8 +204,14 @@ export function getPipelineStatsAggregation({ const start = version.lastSeen - timeseriesInterval * 1000; const end = version.lastSeen; + const dataset = 'node_stats'; + const type = 'logstash_stats'; + const moduleType = 'logstash'; + const query = createQuery({ - types: ['node_stats', 'logstash_stats'], + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, start, end, metric: LogstashMetric.getMetricFields(), @@ -210,7 +223,6 @@ export function getPipelineStatsAggregation({ return fetchPipelineLatestStats( query, - logstashIndexPattern, pipelineId, version, // @ts-ignore not undefined, need to get correct config diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.ts index 0dbbf26b331e2..eecc4388f8947 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.ts @@ -8,7 +8,8 @@ import { get, orderBy } from 'lodash'; import { createQuery } from '../create_query'; import { LogstashMetric } from '../metrics'; -import { checkParam } from '../error_missing_required'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { Globals } from '../../static_globals'; import { LegacyRequest, PipelineVersion } from '../../types'; import { mergePipelineVersions } from './merge_pipeline_versions'; @@ -61,17 +62,23 @@ const createScopedAgg = (pipelineId: string, maxBucketSize: number) => { function fetchPipelineVersions({ req, - lsIndexPattern, clusterUuid, pipelineId, }: { req: LegacyRequest; - lsIndexPattern: string; clusterUuid: string; pipelineId: string; }) { + const dataset = 'node_stats'; + const type = 'logstash_stats'; + const moduleType = 'logstash'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + dataset, + }); const config = req.server.config(); - checkParam(lsIndexPattern, 'logstashIndexPattern in getPipelineVersions'); const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); const filters = [ @@ -105,7 +112,9 @@ function fetchPipelineVersions({ }, ]; const query = createQuery({ - types: ['node_stats', 'logstash_stats'], + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, metric: LogstashMetric.getMetricFields(), clusterUuid, filters, @@ -121,7 +130,7 @@ function fetchPipelineVersions({ }; const params = { - index: lsIndexPattern, + index: indexPatterns, size: 0, ignore_unavailable: true, body: { @@ -159,7 +168,6 @@ export function _handleResponse(response: any) { export async function getPipelineVersions(args: { req: LegacyRequest; - lsIndexPattern: string; clusterUuid: string; pipelineId: string; }) { diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex.ts index 75b82787ef3d0..5605b23b8988b 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex.ts @@ -7,7 +7,6 @@ import boom from '@hapi/boom'; import { get } from 'lodash'; -import { checkParam } from '../error_missing_required'; import { getPipelineStateDocument } from './get_pipeline_state_document'; import { getPipelineVertexStatsAggregation } from './get_pipeline_vertex_stats_aggregation'; import { calculateTimeseriesInterval } from '../calculate_timeseries_interval'; @@ -137,14 +136,11 @@ export function _enrichVertexStateWithStatsAggregation( export async function getPipelineVertex( req: LegacyRequest, config: { get: (key: string) => string | undefined }, - lsIndexPattern: string, clusterUuid: string, pipelineId: string, version: PipelineVersion, vertexId: string ) { - checkParam(lsIndexPattern, 'lsIndexPattern in getPipeline'); - // Determine metrics' timeseries interval based on version's timespan const minIntervalSeconds = Math.max(Number(config.get('monitoring.ui.min_interval_seconds')), 30); const timeseriesInterval = calculateTimeseriesInterval( @@ -156,14 +152,12 @@ export async function getPipelineVertex( const [stateDocument, statsAggregation] = await Promise.all([ getPipelineStateDocument({ req, - logstashIndexPattern: lsIndexPattern, clusterUuid, pipelineId, version, }), getPipelineVertexStatsAggregation({ req, - logstashIndexPattern: lsIndexPattern, timeSeriesIntervalInSeconds: timeseriesInterval, clusterUuid, pipelineId, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.ts index bfd803f0a86d5..8b26f5d44855b 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.ts @@ -6,8 +6,10 @@ */ import { LegacyRequest, PipelineVersion } from '../../types'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; import { createQuery } from '../create_query'; import { LogstashMetric } from '../metrics'; +import { Globals } from '../../static_globals'; function scalarCounterAggregation( field: string, @@ -147,7 +149,6 @@ function createTimeSeriesAgg(timeSeriesIntervalInSeconds: number, ...aggsList: o function fetchPipelineVertexTimeSeriesStats({ query, - logstashIndexPattern, pipelineId, version, vertexId, @@ -157,7 +158,6 @@ function fetchPipelineVertexTimeSeriesStats({ req, }: { query: object; - logstashIndexPattern: string; pipelineId: string; version: PipelineVersion; vertexId: string; @@ -174,8 +174,14 @@ function fetchPipelineVertexTimeSeriesStats({ }), }; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType: 'logstash', + }); + const params = { - index: logstashIndexPattern, + index: indexPatterns, size: 0, ignore_unavailable: true, filter_path: [ @@ -202,7 +208,6 @@ function fetchPipelineVertexTimeSeriesStats({ export function getPipelineVertexStatsAggregation({ req, - logstashIndexPattern, timeSeriesIntervalInSeconds, clusterUuid, pipelineId, @@ -210,7 +215,6 @@ export function getPipelineVertexStatsAggregation({ vertexId, }: { req: LegacyRequest; - logstashIndexPattern: string; timeSeriesIntervalInSeconds: number; clusterUuid: string; pipelineId: string; @@ -252,8 +256,14 @@ export function getPipelineVertexStatsAggregation({ const start = version.firstSeen; const end = version.lastSeen; + const moduleType = 'logstash'; + const dataset = 'node_stats'; + const type = 'logstash_stats'; + const query = createQuery({ - types: ['node_stats', 'logstash_stats'], + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, start, end, metric: LogstashMetric.getMetricFields(), @@ -265,7 +275,6 @@ export function getPipelineVertexStatsAggregation({ return fetchPipelineVertexTimeSeriesStats({ query, - logstashIndexPattern, pipelineId, version, vertexId, diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.js b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.js index 1029f00455c69..bbcedf7fdd33d 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.js +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.js @@ -34,9 +34,6 @@ const mockReq = ( if (prop === 'server.uuid') { return 'kibana-1234'; } - if (prop === 'monitoring.ui.metricbeat.index') { - return 'metricbeat-*'; - } }), }; }, diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.ts b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.ts index eda9315842040..463ccf547d5db 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.ts +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.ts @@ -291,16 +291,6 @@ function getUuidBucketName(productName: string) { } } -function matchesMetricbeatIndex(metricbeatIndex: string, index: string) { - if (index.includes(metricbeatIndex)) { - return true; - } - if (metricbeatIndex.includes('*')) { - return new RegExp(metricbeatIndex).test(index); - } - return false; -} - function isBeatFromAPM(bucket: Bucket) { const beatType = get(bucket, 'single_type.beat_type'); if (!beatType) { @@ -443,7 +433,6 @@ export const getCollectionStatus = async ( ) => { const config = req.server.config(); const kibanaUuid = config.get('server.uuid'); - const metricbeatIndex = config.get('monitoring.ui.metricbeat.index')!; const size = config.get('monitoring.ui.max_bucket_size'); const hasPermissions = await hasNecessaryPermissions(req); @@ -484,11 +473,6 @@ export const getCollectionStatus = async ( if (bucket.key.includes(token)) { return true; } - if (matchesMetricbeatIndex(metricbeatIndex, bucket.key)) { - if (get(bucket, `${uuidBucketName}.buckets`, []).length) { - return true; - } - } return false; }); @@ -512,9 +496,7 @@ export const getCollectionStatus = async ( // If there is a single bucket, then they are fully migrated or fully on the internal collector else if (indexBuckets.length === 1) { const singleIndexBucket = indexBuckets[0]; - const isFullyMigrated = - singleIndexBucket.key.includes(METRICBEAT_INDEX_NAME_UNIQUE_TOKEN) || - matchesMetricbeatIndex(metricbeatIndex, singleIndexBucket.key); + const isFullyMigrated = singleIndexBucket.key.includes(METRICBEAT_INDEX_NAME_UNIQUE_TOKEN); const map = isFullyMigrated ? fullyMigratedUuidsMap : internalCollectorsUuidsMap; const uuidBuckets = get(singleIndexBucket, `${uuidBucketName}.buckets`, []); @@ -594,8 +576,7 @@ export const getCollectionStatus = async ( for (const indexBucket of indexBuckets) { const isFullyMigrated = considerAllInstancesMigrated || - indexBucket.key.includes(METRICBEAT_INDEX_NAME_UNIQUE_TOKEN) || - matchesMetricbeatIndex(metricbeatIndex, indexBucket.key); + indexBucket.key.includes(METRICBEAT_INDEX_NAME_UNIQUE_TOKEN); const map = isFullyMigrated ? fullyMigratedUuidsMap : internalCollectorsUuidsMap; const otherMap = !isFullyMigrated ? fullyMigratedUuidsMap : internalCollectorsUuidsMap; diff --git a/x-pack/plugins/monitoring/server/lib/standalone_clusters/has_standalone_clusters.ts b/x-pack/plugins/monitoring/server/lib/standalone_clusters/has_standalone_clusters.ts index 4aacc6d14d0f9..1859c66e9e713 100644 --- a/x-pack/plugins/monitoring/server/lib/standalone_clusters/has_standalone_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/standalone_clusters/has_standalone_clusters.ts @@ -9,8 +9,22 @@ import moment from 'moment'; import { get } from 'lodash'; import { LegacyRequest } from '../../types'; import { standaloneClusterFilter } from './'; +import { Globals } from '../../static_globals'; +import { getLegacyIndexPattern, getNewIndexPatterns } from '../cluster/get_index_patterns'; -export async function hasStandaloneClusters(req: LegacyRequest, indexPatterns: string[]) { +export async function hasStandaloneClusters(req: LegacyRequest, ccs: string) { + const lsIndexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType: 'logstash', + ccs, + }); + // use legacy because no integration exists for beats + const beatsIndexPatterns = getLegacyIndexPattern({ + moduleType: 'beats', + config: Globals.app.config, + ccs, + }); + const indexPatterns = [lsIndexPatterns, beatsIndexPatterns]; const indexPatternList = indexPatterns.reduce((list, patterns) => { list.push(...patterns.split(',')); return list; @@ -28,7 +42,12 @@ export async function hasStandaloneClusters(req: LegacyRequest, indexPatterns: s }, { terms: { - 'event.dataset': ['logstash.node.stats', 'logstash.node', 'beat.stats', 'beat.state'], + 'metricset.name': ['node', 'node_stats', 'stats', 'state'], + }, + }, + { + terms: { + 'data_stream.dataset': ['node', 'node_stats', 'stats', 'state'], }, }, ], diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/_get_apm_cluster_status.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/_get_apm_cluster_status.js index a28312de78af0..bd0198ffcc3b2 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/_get_apm_cluster_status.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/_get_apm_cluster_status.js @@ -7,9 +7,9 @@ import { getApmsForClusters } from '../../../../lib/apm/get_apms_for_clusters'; -export const getApmClusterStatus = (req, apmIndexPattern, { clusterUuid }) => { +export const getApmClusterStatus = (req, { clusterUuid }) => { const clusters = [{ cluster_uuid: clusterUuid }]; - return getApmsForClusters(req, apmIndexPattern, clusters).then((apms) => { + return getApmsForClusters(req, clusters).then((apms) => { const [{ stats, config }] = apms; return { ...stats, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js index a0b00167101fe..4fa0de7d399d9 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js @@ -47,9 +47,7 @@ export function apmInstanceRoute(server) { try { const [metrics, apmSummary] = await Promise.all([ - getMetrics(req, apmIndexPattern, metricSet, [ - { term: { 'beats_stats.beat.uuid': apmUuid } }, - ]), + getMetrics(req, 'beats', metricSet, [{ term: { 'beats_stats.beat.uuid': apmUuid } }]), getApmInfo(req, apmIndexPattern, { clusterUuid, apmUuid }), ]); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js index ea7f3f41b842e..b773cfd7b0fb9 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js @@ -6,12 +6,10 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { metricSet } from './metric_set_overview'; import { handleError } from '../../../../lib/errors'; import { getApmClusterStatus } from './_get_apm_cluster_status'; -import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; export function apmOverviewRoute(server) { server.route({ @@ -33,9 +31,7 @@ export function apmOverviewRoute(server) { }, async handler(req) { const config = server.config(); - const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; - const apmIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_BEATS, ccs); const showCgroupMetrics = config.get('monitoring.ui.container.apm.enabled'); if (showCgroupMetrics) { @@ -45,8 +41,8 @@ export function apmOverviewRoute(server) { try { const [stats, metrics] = await Promise.all([ - getApmClusterStatus(req, apmIndexPattern, { clusterUuid }), - getMetrics(req, apmIndexPattern, metricSet), + getApmClusterStatus(req, { clusterUuid }), + getMetrics(req, 'beats', metricSet), ]); return { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js index 851380fede77d..d9454b7ae62cf 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js @@ -49,9 +49,7 @@ export function beatsDetailRoute(server) { try { const [summary, metrics] = await Promise.all([ getBeatSummary(req, beatsIndexPattern, summaryOptions), - getMetrics(req, beatsIndexPattern, metricSet, [ - { term: { 'beats_stats.beat.uuid': beatUuid } }, - ]), + getMetrics(req, 'beats', metricSet, [{ term: { 'beats_stats.beat.uuid': beatUuid } }]), ]); return { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js index 4abf46b3ad1ce..12349c8b85571 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js @@ -41,7 +41,7 @@ export function beatsOverviewRoute(server) { const [latest, stats, metrics] = await Promise.all([ getLatestStats(req, beatsIndexPattern, clusterUuid), getStats(req, beatsIndexPattern, clusterUuid), - getMetrics(req, beatsIndexPattern, metricSet), + getMetrics(req, 'beats', metricSet), ]); return { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts index d07a660222407..a1d76fe5ccd0d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts @@ -13,9 +13,10 @@ import { handleError } from '../../../../lib/errors/handle_error'; import { prefixIndexPattern } from '../../../../../common/ccs_utils'; // @ts-ignore import { getMetrics } from '../../../../lib/details/get_metrics'; -import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { ElasticsearchResponse } from '../../../../../common/types/es'; import { LegacyRequest } from '../../../../types'; +import { getNewIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; +import { Globals } from '../../../../static_globals'; function getFormattedLeaderIndex(leaderIndex: string) { let leader = leaderIndex; @@ -98,27 +99,33 @@ export function ccrShardRoute(server: { route: (p: any) => void; config: () => { }, }, async handler(req: LegacyRequest) { - const config = server.config(); const index = req.params.index; const shardId = req.params.shardId; - const ccs = req.payload.ccs; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); + const moduleType = 'elasticsearch'; + const dataset = 'ccr'; + const esIndexPattern = getNewIndexPatterns({ + config: Globals.app.config, + ccs: req.payload.ccs, + moduleType, + dataset, + }); const filters = [ { bool: { should: [ + { term: { 'data_stream.dataset': { value: `${moduleType}.${dataset}` } } }, { term: { - type: { - value: 'ccr_stats', + 'metricset.name': { + value: dataset, }, }, }, { term: { - 'metricset.name': { - value: 'ccr', + type: { + value: 'ccr_stats', }, }, }, @@ -145,7 +152,7 @@ export function ccrShardRoute(server: { route: (p: any) => void; config: () => { const [metrics, ccrResponse]: [unknown, ElasticsearchResponse] = await Promise.all([ getMetrics( req, - esIndexPattern, + 'elasticsearch', [ { keys: ['ccr_sync_lag_time'], name: 'ccr_sync_lag_time' }, { keys: ['ccr_sync_lag_ops'], name: 'ccr_sync_lag_ops' }, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js index e99ae04ab282c..6c6b1487da106 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js @@ -14,7 +14,6 @@ import { getShardAllocation, getShardStats } from '../../../../lib/elasticsearch import { handleError } from '../../../../lib/errors/handle_error'; import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSet } from './metric_set_index_detail'; -import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs/get_logs'; const { advanced: metricSetAdvanced, overview: metricSetOverview } = metricSet; @@ -42,12 +41,10 @@ export function esIndexRoute(server) { handler: async (req) => { try { const config = server.config(); - const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; const indexUuid = req.params.id; const start = req.payload.timeRange.min; const end = req.payload.timeRange.max; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); const filebeatIndexPattern = prefixIndexPattern( config, config.get('monitoring.ui.logs.index'), @@ -57,21 +54,21 @@ export function esIndexRoute(server) { const isAdvanced = req.payload.is_advanced; const metricSet = isAdvanced ? metricSetAdvanced : metricSetOverview; - const cluster = await getClusterStats(req, esIndexPattern, clusterUuid); + const cluster = await getClusterStats(req, clusterUuid); const showSystemIndices = true; // hardcode to true, because this could be a system index - const shardStats = await getShardStats(req, esIndexPattern, cluster, { + const shardStats = await getShardStats(req, cluster, { includeNodes: true, includeIndices: true, indexName: indexUuid, }); - const indexSummary = await getIndexSummary(req, esIndexPattern, shardStats, { + const indexSummary = await getIndexSummary(req, shardStats, { clusterUuid, indexUuid, start, end, }); - const metrics = await getMetrics(req, esIndexPattern, metricSet, [ + const metrics = await getMetrics(req, 'elasticsearch', metricSet, [ { term: { 'index_stats.index': indexUuid } }, ]); @@ -97,7 +94,7 @@ export function esIndexRoute(server) { stateUuid, showSystemIndices, }; - const shards = await getShardAllocation(req, esIndexPattern, allocationOptions); + const shards = await getShardAllocation(req, allocationOptions); logs = await getLogs(config, req, filebeatIndexPattern, { clusterUuid, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js index 76e769ac030ba..ea490a1547116 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js @@ -10,8 +10,6 @@ import { getClusterStats } from '../../../../lib/cluster/get_cluster_stats'; import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getIndices } from '../../../../lib/elasticsearch/indices'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../../common/ccs_utils'; -import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getIndicesUnassignedShardStats } from '../../../../lib/elasticsearch/shards/get_indices_unassigned_shard_stats'; export function esIndicesRoute(server) { @@ -36,25 +34,14 @@ export function esIndicesRoute(server) { }, }, async handler(req) { - const config = server.config(); const { clusterUuid } = req.params; const { show_system_indices: showSystemIndices } = req.query; const { ccs } = req.payload; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); try { - const clusterStats = await getClusterStats(req, esIndexPattern, clusterUuid); - const indicesUnassignedShardStats = await getIndicesUnassignedShardStats( - req, - esIndexPattern, - clusterStats - ); - const indices = await getIndices( - req, - esIndexPattern, - showSystemIndices, - indicesUnassignedShardStats - ); + const clusterStats = await getClusterStats(req, clusterUuid, ccs); + const indicesUnassignedShardStats = await getIndicesUnassignedShardStats(req, clusterStats); + const indices = await getIndices(req, showSystemIndices, indicesUnassignedShardStats); return { clusterStatus: getClusterStatus(clusterStats, indicesUnassignedShardStats), diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js index 5853cc3d6ee9d..d27ec8ce3cc83 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js @@ -10,8 +10,6 @@ import { getClusterStats } from '../../../../lib/cluster/get_cluster_stats'; import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getMlJobs } from '../../../../lib/elasticsearch/get_ml_jobs'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../../common/ccs_utils'; -import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getIndicesUnassignedShardStats } from '../../../../lib/elasticsearch/shards/get_indices_unassigned_shard_stats'; export function mlJobRoute(server) { @@ -33,20 +31,12 @@ export function mlJobRoute(server) { }, }, async handler(req) { - const config = server.config(); - const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); try { - const clusterStats = await getClusterStats(req, esIndexPattern, clusterUuid); - const indicesUnassignedShardStats = await getIndicesUnassignedShardStats( - req, - esIndexPattern, - clusterStats - ); - const rows = await getMlJobs(req, esIndexPattern); - + const clusterStats = await getClusterStats(req, clusterUuid); + const indicesUnassignedShardStats = await getIndicesUnassignedShardStats(req, clusterStats); + const rows = await getMlJobs(req); return { clusterStatus: getClusterStatus(clusterStats, indicesUnassignedShardStats), rows, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js index 5f77d0394a4f1..86048ed1765ab 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js @@ -14,7 +14,6 @@ import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors/handle_error'; import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSets } from './metric_set_node_detail'; -import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs/get_logs'; const { advanced: metricSetAdvanced, overview: metricSetOverview } = metricSets; @@ -48,7 +47,6 @@ export function esNodeRoute(server) { const nodeUuid = req.params.nodeUuid; const start = req.payload.timeRange.min; const end = req.payload.timeRange.max; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); const filebeatIndexPattern = prefixIndexPattern( config, config.get('monitoring.ui.logs.index'), @@ -75,25 +73,26 @@ export function esNodeRoute(server) { } try { - const cluster = await getClusterStats(req, esIndexPattern, clusterUuid); + const cluster = await getClusterStats(req, clusterUuid, ccs); const clusterState = get( cluster, 'cluster_state', get(cluster, 'elasticsearch.cluster.stats.state') ); - const shardStats = await getShardStats(req, esIndexPattern, cluster, { + + const shardStats = await getShardStats(req, cluster, { includeIndices: true, includeNodes: true, nodeUuid, }); - const nodeSummary = await getNodeSummary(req, esIndexPattern, clusterState, shardStats, { + const nodeSummary = await getNodeSummary(req, clusterState, shardStats, { clusterUuid, nodeUuid, start, end, }); - const metrics = await getMetrics(req, esIndexPattern, metricSet, [ + const metrics = await getMetrics(req, 'elasticsearch', metricSet, [ { term: { 'source_node.uuid': nodeUuid } }, ]); let logs; @@ -118,7 +117,7 @@ export function esNodeRoute(server) { stateUuid, showSystemIndices, }; - const shards = await getShardAllocation(req, esIndexPattern, allocationOptions); + const shards = await getShardAllocation(req, allocationOptions); shardAllocation = { shards, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js index 7ea2e6e1e1440..d6cff7ecd9ae9 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js @@ -11,8 +11,6 @@ import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getNodes } from '../../../../lib/elasticsearch/nodes'; import { getNodesShardCount } from '../../../../lib/elasticsearch/shards/get_nodes_shard_count'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../../common/ccs_utils'; -import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getPaginatedNodes } from '../../../../lib/elasticsearch/nodes/get_nodes/get_paginated_nodes'; import { LISTING_METRICS_NAMES } from '../../../../lib/elasticsearch/nodes/get_nodes/nodes_listing_metrics'; import { getIndicesUnassignedShardStats } from '../../../../lib/elasticsearch/shards/get_indices_unassigned_shard_stats'; @@ -45,25 +43,18 @@ export function esNodesRoute(server) { }, }, async handler(req) { - const config = server.config(); const { ccs, pagination, sort, queryText } = req.payload; const clusterUuid = req.params.clusterUuid; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); try { - const clusterStats = await getClusterStats(req, esIndexPattern, clusterUuid); - const nodesShardCount = await getNodesShardCount(req, esIndexPattern, clusterStats); - const indicesUnassignedShardStats = await getIndicesUnassignedShardStats( - req, - esIndexPattern, - clusterStats - ); + const clusterStats = await getClusterStats(req, clusterUuid); + const nodesShardCount = await getNodesShardCount(req, clusterStats); + const indicesUnassignedShardStats = await getIndicesUnassignedShardStats(req, clusterStats); const clusterStatus = getClusterStatus(clusterStats, indicesUnassignedShardStats); const metricSet = LISTING_METRICS_NAMES; const { pageOfNodes, totalNodeCount } = await getPaginatedNodes( req, - esIndexPattern, { clusterUuid }, metricSet, pagination, @@ -72,16 +63,11 @@ export function esNodesRoute(server) { { clusterStats, nodesShardCount, - } + }, + ccs ); - const nodes = await getNodes( - req, - esIndexPattern, - pageOfNodes, - clusterStats, - nodesShardCount - ); + const nodes = await getNodes(req, pageOfNodes, clusterStats, nodesShardCount); return { clusterStatus, nodes, totalNodeCount }; } catch (err) { throw handleError(err, req); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js index a84ca61a9396b..6c74f9dd872d2 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js @@ -13,10 +13,6 @@ import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors/handle_error'; import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSet } from './metric_set_overview'; -import { - INDEX_PATTERN_ELASTICSEARCH, - INDEX_PATTERN_ELASTICSEARCH_ECS, -} from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs'; import { getIndicesUnassignedShardStats } from '../../../../lib/elasticsearch/shards/get_indices_unassigned_shard_stats'; @@ -40,22 +36,7 @@ export function esOverviewRoute(server) { }, async handler(req) { const config = server.config(); - const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); - const esLegacyIndexPattern = prefixIndexPattern( - config, - INDEX_PATTERN_ELASTICSEARCH, - ccs, - true - ); - const esEcsIndexPattern = prefixIndexPattern( - config, - INDEX_PATTERN_ELASTICSEARCH_ECS, - ccs, - true - ); - const filebeatIndexPattern = prefixIndexPattern( config, config.get('monitoring.ui.logs.index'), @@ -68,21 +49,12 @@ export function esOverviewRoute(server) { try { const [clusterStats, metrics, shardActivity, logs] = await Promise.all([ - getClusterStats(req, esIndexPattern, clusterUuid), - getMetrics(req, esIndexPattern, metricSet), - getLastRecovery( - req, - esLegacyIndexPattern, - esEcsIndexPattern, - config.get('monitoring.ui.max_bucket_size') - ), + getClusterStats(req, clusterUuid), + getMetrics(req, 'elasticsearch', metricSet), + getLastRecovery(req, config.get('monitoring.ui.max_bucket_size')), getLogs(config, req, filebeatIndexPattern, { clusterUuid, start, end }), ]); - const indicesUnassignedShardStats = await getIndicesUnassignedShardStats( - req, - esIndexPattern, - clusterStats - ); + const indicesUnassignedShardStats = await getIndicesUnassignedShardStats(req, clusterStats); const result = { clusterStatus: getClusterStatus(clusterStats, indicesUnassignedShardStats), diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts index eee6ba98e62c7..46626066005ad 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts @@ -89,9 +89,9 @@ export function internalMonitoringCheckRoute(server: LegacyServer, npRoute: Rout const config = server.config(); const { ccs } = request.body; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs, true); - const kbnIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_KIBANA, ccs, true); - const lsIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_LOGSTASH, ccs, true); + const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); + const kbnIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_KIBANA, ccs); + const lsIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_LOGSTASH, ccs); const indexCounts = await Promise.all([ checkLatestMonitoringIsLegacy(context, esIndexPattern), checkLatestMonitoringIsLegacy(context, kbnIndexPattern), diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/overview.js index b9bc0f49bc99d..97ab818a82992 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/overview.js @@ -6,11 +6,9 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { metricSet } from './metric_set_overview'; import { handleError } from '../../../../lib/errors'; -import { INDEX_PATTERN_ENTERPRISE_SEARCH } from '../../../../../common/constants'; import { getStats } from '../../../../lib/enterprise_search'; export function entSearchOverviewRoute(server) { @@ -34,16 +32,10 @@ export function entSearchOverviewRoute(server) { async handler(req) { const clusterUuid = req.params.clusterUuid; - const entSearchIndexPattern = prefixIndexPattern( - server.config(), - INDEX_PATTERN_ENTERPRISE_SEARCH, - req.payload.ccs - ); - try { const [stats, metrics] = await Promise.all([ - getStats(req, entSearchIndexPattern, clusterUuid), - getMetrics(req, entSearchIndexPattern, metricSet, [], { + getStats(req, clusterUuid), + getMetrics(req, 'enterprisesearch', metricSet, [], { skipClusterUuidFilter: true, }), ]); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/_get_kibana_cluster_status.js b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/_get_kibana_cluster_status.js index 6a54e19d3493b..373ab2cbde9b0 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/_get_kibana_cluster_status.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/_get_kibana_cluster_status.js @@ -8,9 +8,7 @@ import { get } from 'lodash'; import { getKibanasForClusters } from '../../../../lib/kibana/get_kibanas_for_clusters'; -export const getKibanaClusterStatus = (req, kbnIndexPattern, { clusterUuid }) => { +export const getKibanaClusterStatus = (req, { clusterUuid }) => { const clusters = [{ cluster_uuid: clusterUuid }]; - return getKibanasForClusters(req, kbnIndexPattern, clusters).then((kibanas) => - get(kibanas, '[0].stats') - ); + return getKibanasForClusters(req, clusters).then((kibanas) => get(kibanas, '[0].stats')); }; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts index 613ca39275c2d..dfbd8e82bb29b 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts @@ -12,10 +12,7 @@ import { handleError } from '../../../../lib/errors'; // @ts-ignore import { getMetrics } from '../../../../lib/details/get_metrics'; // @ts-ignore -import { prefixIndexPattern } from '../../../../../common/ccs_utils'; -// @ts-ignore import { metricSet } from './metric_set_instance'; -import { INDEX_PATTERN_KIBANA } from '../../../../../common/constants'; import { LegacyRequest, LegacyServer } from '../../../../types'; /** @@ -44,16 +41,13 @@ export function kibanaInstanceRoute(server: LegacyServer) { }, }, async handler(req: LegacyRequest) { - const config = server.config(); - const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; const kibanaUuid = req.params.kibanaUuid; - const kbnIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_KIBANA, ccs); try { const [metrics, kibanaSummary] = await Promise.all([ - getMetrics(req, kbnIndexPattern, metricSet), - getKibanaInfo(req, kbnIndexPattern, { clusterUuid, kibanaUuid }), + getMetrics(req, 'kibana', metricSet), + getKibanaInfo(req, { clusterUuid, kibanaUuid }), ]); return { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js index f9b3498cd684e..f1e872d6436f2 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js @@ -6,11 +6,9 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getKibanaClusterStatus } from './_get_kibana_cluster_status'; import { getKibanas } from '../../../../lib/kibana/get_kibanas'; import { handleError } from '../../../../lib/errors'; -import { INDEX_PATTERN_KIBANA } from '../../../../../common/constants'; export function kibanaInstancesRoute(server) { /** @@ -34,15 +32,12 @@ export function kibanaInstancesRoute(server) { }, }, async handler(req) { - const config = server.config(); - const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; - const kbnIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_KIBANA, ccs); try { const [clusterStatus, kibanas] = await Promise.all([ - getKibanaClusterStatus(req, kbnIndexPattern, { clusterUuid }), - getKibanas(req, kbnIndexPattern, { clusterUuid }), + getKibanaClusterStatus(req, { clusterUuid }), + getKibanas(req, { clusterUuid }), ]); return { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js index f9a9443c3533b..8f77dea99868a 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js @@ -6,12 +6,10 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getKibanaClusterStatus } from './_get_kibana_cluster_status'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { metricSet } from './metric_set_overview'; import { handleError } from '../../../../lib/errors'; -import { INDEX_PATTERN_KIBANA } from '../../../../../common/constants'; export function kibanaOverviewRoute(server) { /** @@ -35,20 +33,20 @@ export function kibanaOverviewRoute(server) { }, }, async handler(req) { - const config = server.config(); - const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; - const kbnIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_KIBANA, ccs); try { + const moduleType = 'kibana'; + const dsDataset = 'stats'; const [clusterStatus, metrics] = await Promise.all([ - getKibanaClusterStatus(req, kbnIndexPattern, { clusterUuid }), - getMetrics(req, kbnIndexPattern, metricSet, [ + getKibanaClusterStatus(req, { clusterUuid }), + getMetrics(req, moduleType, metricSet, [ { bool: { should: [ + { term: { 'data_stream.dataset': `${moduleType}.${dsDataset}` } }, + { term: { 'metricset.name': dsDataset } }, { term: { type: 'kibana_stats' } }, - { term: { 'metricset.name': 'stats' } }, ], }, }, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js index 6c0ec3f0af374..9c7749bf74903 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js @@ -9,9 +9,7 @@ import { schema } from '@kbn/config-schema'; import { getNodeInfo } from '../../../../lib/logstash/get_node_info'; import { handleError } from '../../../../lib/errors'; import { getMetrics } from '../../../../lib/details/get_metrics'; -import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSets } from './metric_set_node'; -import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; const { advanced: metricSetAdvanced, overview: metricSetOverview } = metricSets; @@ -50,9 +48,7 @@ export function logstashNodeRoute(server) { }, async handler(req) { const config = server.config(); - const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; - const lsIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_LOGSTASH, ccs); const logstashUuid = req.params.logstashUuid; let metricSet; @@ -71,18 +67,21 @@ export function logstashNodeRoute(server) { } try { + const moduleType = 'logstash'; + const dsDataset = 'node_stats'; const [metrics, nodeSummary] = await Promise.all([ - getMetrics(req, lsIndexPattern, metricSet, [ + getMetrics(req, 'logstash', metricSet, [ { bool: { should: [ + { term: { 'data_stream.dataset': `${moduleType}.${dsDataset}` } }, + { term: { 'metricset.name': dsDataset } }, { term: { type: 'logstash_stats' } }, - { term: { 'metricset.name': 'node_stats' } }, ], }, }, ]), - getNodeInfo(req, lsIndexPattern, { clusterUuid, logstashUuid }), + getNodeInfo(req, { clusterUuid, logstashUuid }), ]); return { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js index 051fb7d38fd41..cca02519aa7c4 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js @@ -51,7 +51,7 @@ export function logstashNodesRoute(server) { try { const [clusterStatus, nodes] = await Promise.all([ - getClusterStatus(req, lsIndexPattern, { clusterUuid }), + getClusterStatus(req, { clusterUuid }), getNodes(req, lsIndexPattern, { clusterUuid }), ]); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js index 7ecd09a9d993e..81d64e8fcdc2b 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js @@ -9,9 +9,7 @@ import { schema } from '@kbn/config-schema'; import { getClusterStatus } from '../../../../lib/logstash/get_cluster_status'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSet } from './metric_set_overview'; -import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; /* * Logstash Overview route. @@ -45,24 +43,24 @@ export function logstashOverviewRoute(server) { }, }, async handler(req) { - const config = server.config(); - const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; - const lsIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_LOGSTASH, ccs); try { + const moduleType = 'logstash'; + const dsDataset = 'node_stats'; const [metrics, clusterStatus] = await Promise.all([ - getMetrics(req, lsIndexPattern, metricSet, [ + getMetrics(req, moduleType, metricSet, [ { bool: { should: [ + { term: { 'data_stream.dataset': `${moduleType}.${dsDataset}` } }, + { term: { 'metricset.name': dsDataset } }, { term: { type: 'logstash_stats' } }, - { term: { 'metricset.name': 'node_stats' } }, ], }, }, ]), - getClusterStatus(req, lsIndexPattern, { clusterUuid }), + getClusterStatus(req, { clusterUuid }), ]); return { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js index 6b81059f0c256..59f91b82ca497 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js @@ -10,8 +10,6 @@ import { handleError } from '../../../../lib/errors'; import { getPipelineVersions } from '../../../../lib/logstash/get_pipeline_versions'; import { getPipeline } from '../../../../lib/logstash/get_pipeline'; import { getPipelineVertex } from '../../../../lib/logstash/get_pipeline_vertex'; -import { prefixIndexPattern } from '../../../../../common/ccs_utils'; -import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; function getPipelineVersion(versions, pipelineHash) { return pipelineHash ? versions.find(({ hash }) => hash === pipelineHash) : versions[0]; @@ -48,10 +46,8 @@ export function logstashPipelineRoute(server) { }, handler: async (req) => { const config = server.config(); - const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; const detailVertexId = req.payload.detailVertexId; - const lsIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_LOGSTASH, ccs); const pipelineId = req.params.pipelineId; // Optional params default to empty string, set to null to be more explicit. @@ -62,8 +58,6 @@ export function logstashPipelineRoute(server) { try { versions = await getPipelineVersions({ req, - config, - lsIndexPattern, clusterUuid, pipelineId, }); @@ -72,18 +66,10 @@ export function logstashPipelineRoute(server) { } const version = getPipelineVersion(versions, pipelineHash); - const promises = [getPipeline(req, config, lsIndexPattern, clusterUuid, pipelineId, version)]; + const promises = [getPipeline(req, config, clusterUuid, pipelineId, version)]; if (detailVertexId) { promises.push( - getPipelineVertex( - req, - config, - lsIndexPattern, - clusterUuid, - pipelineId, - version, - detailVertexId - ) + getPipelineVertex(req, config, clusterUuid, pipelineId, version, detailVertexId) ); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js index b7d86e86e7a07..ace5661b9bf98 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js @@ -8,8 +8,6 @@ import { schema } from '@kbn/config-schema'; import { getClusterStatus } from '../../../../../lib/logstash/get_cluster_status'; import { handleError } from '../../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; -import { INDEX_PATTERN_LOGSTASH } from '../../../../../../common/constants'; import { getPaginatedPipelines } from '../../../../../lib/logstash/get_paginated_pipelines'; /** @@ -45,10 +43,8 @@ export function logstashClusterPipelinesRoute(server) { }, }, handler: async (req) => { - const config = server.config(); - const { ccs, pagination, sort, queryText } = req.payload; + const { pagination, sort, queryText } = req.payload; const clusterUuid = req.params.clusterUuid; - const lsIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_LOGSTASH, ccs); const throughputMetric = 'logstash_cluster_pipeline_throughput'; const nodesCountMetric = 'logstash_cluster_pipeline_nodes_count'; @@ -64,7 +60,6 @@ export function logstashClusterPipelinesRoute(server) { try { const response = await getPaginatedPipelines({ req, - lsIndexPattern, clusterUuid, metrics: { throughputMetric, nodesCountMetric }, pagination, @@ -74,7 +69,7 @@ export function logstashClusterPipelinesRoute(server) { return { ...response, - clusterStatus: await getClusterStatus(req, lsIndexPattern, { clusterUuid }), + clusterStatus: await getClusterStatus(req, { clusterUuid }), }; } catch (err) { throw handleError(err, req); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js index f31e88b5b8b08..c232da925e74c 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js @@ -8,8 +8,6 @@ import { schema } from '@kbn/config-schema'; import { getNodeInfo } from '../../../../../lib/logstash/get_node_info'; import { handleError } from '../../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; -import { INDEX_PATTERN_LOGSTASH } from '../../../../../../common/constants'; import { getPaginatedPipelines } from '../../../../../lib/logstash/get_paginated_pipelines'; /** @@ -46,10 +44,8 @@ export function logstashNodePipelinesRoute(server) { }, }, handler: async (req) => { - const config = server.config(); - const { ccs, pagination, sort, queryText } = req.payload; + const { pagination, sort, queryText } = req.payload; const { clusterUuid, logstashUuid } = req.params; - const lsIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_LOGSTASH, ccs); const throughputMetric = 'logstash_node_pipeline_throughput'; const nodesCountMetric = 'logstash_node_pipeline_nodes_count'; @@ -66,7 +62,6 @@ export function logstashNodePipelinesRoute(server) { try { const response = await getPaginatedPipelines({ req, - lsIndexPattern, clusterUuid, logstashUuid, metrics: { throughputMetric, nodesCountMetric }, @@ -77,7 +72,7 @@ export function logstashNodePipelinesRoute(server) { return { ...response, - nodeSummary: await getNodeInfo(req, lsIndexPattern, { clusterUuid, logstashUuid }), + nodeSummary: await getNodeInfo(req, { clusterUuid, logstashUuid }), }; } catch (err) { throw handleError(err, req); diff --git a/x-pack/test/api_integration/apis/monitoring/apm/index.js b/x-pack/test/api_integration/apis/monitoring/apm/index.js index ae0623054dc6b..b56a26d71a83a 100644 --- a/x-pack/test/api_integration/apis/monitoring/apm/index.js +++ b/x-pack/test/api_integration/apis/monitoring/apm/index.js @@ -8,10 +8,10 @@ export default function ({ loadTestFile }) { describe('APM', () => { loadTestFile(require.resolve('./overview')); - loadTestFile(require.resolve('./overview_mb')); + // loadTestFile(require.resolve('./overview_mb')); loadTestFile(require.resolve('./instances')); - loadTestFile(require.resolve('./instances_mb')); + // loadTestFile(require.resolve('./instances_mb')); loadTestFile(require.resolve('./instance')); - loadTestFile(require.resolve('./instance_mb')); + // loadTestFile(require.resolve('./instance_mb')); }); } diff --git a/x-pack/test/api_integration/apis/monitoring/beats/index.js b/x-pack/test/api_integration/apis/monitoring/beats/index.js index 8fe09e457f685..235afdc7fe6b2 100644 --- a/x-pack/test/api_integration/apis/monitoring/beats/index.js +++ b/x-pack/test/api_integration/apis/monitoring/beats/index.js @@ -8,10 +8,10 @@ export default function ({ loadTestFile }) { describe('Beats', () => { loadTestFile(require.resolve('./overview')); - loadTestFile(require.resolve('./overview_mb')); + // loadTestFile(require.resolve('./overview_mb')); loadTestFile(require.resolve('./list')); - loadTestFile(require.resolve('./list_mb')); + // loadTestFile(require.resolve('./list_mb')); loadTestFile(require.resolve('./detail')); - loadTestFile(require.resolve('./detail_mb')); + // loadTestFile(require.resolve('./detail_mb')); }); } diff --git a/x-pack/test/api_integration/apis/monitoring/cluster/index.js b/x-pack/test/api_integration/apis/monitoring/cluster/index.js index fb99f5e0b9605..59af14b049aa7 100644 --- a/x-pack/test/api_integration/apis/monitoring/cluster/index.js +++ b/x-pack/test/api_integration/apis/monitoring/cluster/index.js @@ -8,8 +8,8 @@ export default function ({ loadTestFile }) { describe('cluster', () => { loadTestFile(require.resolve('./list')); - loadTestFile(require.resolve('./list_mb')); + // loadTestFile(require.resolve('./list_mb')); loadTestFile(require.resolve('./overview')); - loadTestFile(require.resolve('./overview_mb')); + // loadTestFile(require.resolve('./overview_mb')); }); } diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/index.js b/x-pack/test/api_integration/apis/monitoring/elasticsearch/index.js index ad864979a15fe..167da3e360c16 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/index.js +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/index.js @@ -8,20 +8,20 @@ export default function ({ loadTestFile }) { describe('Elasticsearch', () => { loadTestFile(require.resolve('./overview')); - loadTestFile(require.resolve('./overview_mb')); + // loadTestFile(require.resolve('./overview_mb')); loadTestFile(require.resolve('./nodes')); - loadTestFile(require.resolve('./nodes_mb')); + // loadTestFile(require.resolve('./nodes_mb')); loadTestFile(require.resolve('./node_detail')); - loadTestFile(require.resolve('./node_detail_mb')); + // loadTestFile(require.resolve('./node_detail_mb')); loadTestFile(require.resolve('./node_detail_advanced')); - loadTestFile(require.resolve('./node_detail_advanced_mb')); + // loadTestFile(require.resolve('./node_detail_advanced_mb')); loadTestFile(require.resolve('./indices')); - loadTestFile(require.resolve('./indices_mb')); + // loadTestFile(require.resolve('./indices_mb')); loadTestFile(require.resolve('./index_detail')); - loadTestFile(require.resolve('./index_detail_mb')); + // loadTestFile(require.resolve('./index_detail_mb')); loadTestFile(require.resolve('./ccr')); - loadTestFile(require.resolve('./ccr_mb')); + // loadTestFile(require.resolve('./ccr_mb')); loadTestFile(require.resolve('./ccr_shard')); - loadTestFile(require.resolve('./ccr_shard_mb')); + // loadTestFile(require.resolve('./ccr_shard_mb')); }); } diff --git a/x-pack/test/api_integration/apis/monitoring/kibana/index.js b/x-pack/test/api_integration/apis/monitoring/kibana/index.js index b54b09102bc1b..f953104d915e8 100644 --- a/x-pack/test/api_integration/apis/monitoring/kibana/index.js +++ b/x-pack/test/api_integration/apis/monitoring/kibana/index.js @@ -8,10 +8,10 @@ export default function ({ loadTestFile }) { describe('Kibana', () => { loadTestFile(require.resolve('./overview')); - loadTestFile(require.resolve('./overview_mb')); + // loadTestFile(require.resolve('./overview_mb')); loadTestFile(require.resolve('./listing')); - loadTestFile(require.resolve('./listing_mb')); + // loadTestFile(require.resolve('./listing_mb')); loadTestFile(require.resolve('./instance')); - loadTestFile(require.resolve('./instance_mb')); + // loadTestFile(require.resolve('./instance_mb')); }); } diff --git a/x-pack/test/api_integration/apis/monitoring/logstash/index.js b/x-pack/test/api_integration/apis/monitoring/logstash/index.js index 0caf7e360ef3a..1a8baacacf066 100644 --- a/x-pack/test/api_integration/apis/monitoring/logstash/index.js +++ b/x-pack/test/api_integration/apis/monitoring/logstash/index.js @@ -8,14 +8,14 @@ export default function ({ loadTestFile }) { describe('Logstash', () => { loadTestFile(require.resolve('./overview')); - loadTestFile(require.resolve('./overview_mb')); + // loadTestFile(require.resolve('./overview_mb')); loadTestFile(require.resolve('./nodes')); - loadTestFile(require.resolve('./nodes_mb')); + // loadTestFile(require.resolve('./nodes_mb')); loadTestFile(require.resolve('./node_detail')); - loadTestFile(require.resolve('./node_detail_mb')); + // loadTestFile(require.resolve('./node_detail_mb')); loadTestFile(require.resolve('./multicluster_pipelines')); - loadTestFile(require.resolve('./multicluster_pipelines_mb')); + // loadTestFile(require.resolve('./multicluster_pipelines_mb')); loadTestFile(require.resolve('./pipelines')); - loadTestFile(require.resolve('./pipelines_mb')); + // loadTestFile(require.resolve('./pipelines_mb')); }); } diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/index.js b/x-pack/test/api_integration/apis/monitoring/setup/collection/index.js index 4690b6d6371d8..1898aedfcab0d 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/index.js +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/index.js @@ -8,13 +8,13 @@ export default function ({ loadTestFile }) { describe('Collection', () => { loadTestFile(require.resolve('./kibana')); - loadTestFile(require.resolve('./kibana_mb')); + // loadTestFile(require.resolve('./kibana_mb')); loadTestFile(require.resolve('./kibana_exclusive')); - loadTestFile(require.resolve('./kibana_exclusive_mb')); + // loadTestFile(require.resolve('./kibana_exclusive_mb')); loadTestFile(require.resolve('./es_and_kibana')); - loadTestFile(require.resolve('./es_and_kibana_mb')); + // loadTestFile(require.resolve('./es_and_kibana_mb')); loadTestFile(require.resolve('./es_and_kibana_exclusive')); - loadTestFile(require.resolve('./es_and_kibana_exclusive_mb')); + // loadTestFile(require.resolve('./es_and_kibana_exclusive_mb')); loadTestFile(require.resolve('./detect_beats')); loadTestFile(require.resolve('./detect_beats_management')); loadTestFile(require.resolve('./detect_logstash')); diff --git a/x-pack/test/functional/es_archives/monitoring/singlecluster_red_platinum_mb/data.json.gz b/x-pack/test/functional/es_archives/monitoring/singlecluster_red_platinum_mb/data.json.gz index 163ed932c541351b8f63d2b0b9445e5fb4e11292..d82f05dd3eb4304717516fecb1401aa8f3f4a4ea 100644 GIT binary patch literal 282241 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

+wKqbn2O{GDQn`BtgJS_!UwN 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 PQ&KBW>~M322f=>l%u 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�VUe7aU|T_7hH+za5C<3d|sZXUJ|s?>QsvMHgdE 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

    qvAxi@V; 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

    sKZ*yeqp(H=hs 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

    -LAtHM3 zR*k-h=v@S1m5?Zl5G6W6^ctNg(OYm2$@la9%-p#%_c!d9cygM}>fECPqYPaC1uN74FcB!+W zrc+4KpsnN;cxfz^&K&{WozuD_D}US03FgQ(4y4tF~n$xk;MII4L{LRkFVvsc+q#LfgSnqRmhpr7YG zFQLA2yiJWOh@0#~M*3{K=<)4U52z{rNmK3(fUV?ju~pD*x8fbPqV&}A#C$uchay8u z>ptRQ6hB(oHW*ok`B=EOx$8htXe)|lpI?*OrfO?h zQeEvfo$C~yfe#3|a`Aw0QO~QjJ_^CWMqZ3xu12@Ss=H7o{M-y3Y=&g|CQaGRRM*Ju zc(XhMw`G{>2ryGvO=l^ulHh#!pi%bP)l$vQ+x;U6VP9<`H^Ou*NVx*)DT1HjY{ctqM&{G50;W{|?7R<5&vTlAY4d3=E-Xg0Sp>`Pc+d;~4JL7I(`gn#S z=`&f)`ChCs%~ec?2dA7o^lR}BM+~cdcj8w6bm90jWHi(fgE%t3JbCUqfR?7x@)D}A--AONZyP;z-sv`cQ{)0 zh!TaBUz6}tx;l8wmKL8Uk$++jZ=rwm2c8K=_iySI+ls|s@3QAtty8n`j1@fd)scEP z!US^zXP%Usn~gT*m?QTFZ3D>2ib8p+lOeDyGxlY+_qUxRj|4Ecz0na3S1NdfN;LUT zsp1wpaZl2+yw?;Da#aBdyhHoy=ZdfEE2;&=tz!|OMj%(^l;VfsFIYbil1zE#cc;K# zq2eeP%&zxxaX%8cb1W*0S)Mm|Lp^11?4Z1I$RChC0Hn)OSBq6K@R>oG*U}HB?IXl$ zd-OBOO?n`CzAP9b^XOxmo(Cu~7a6al+63jXGP}X~)q~olp#{l0xn0TsA+XZR4ZK*y zeJe^iKZ^p$T7a8_bhXG818wL|*W*BL_qBXiYp@wbMR7i;gW5w%$bMi0u&gu8K7UmN zC{zsZZB<_%p!N?4a02^NeMADO_mNu;a_M!FU67Vc^pGjeq7}&`qQ7U6PR8OHA5{SX zc14v>7K9wHs)R%5mHPSoghHkooTlnhJTAH3XM4&9etm1VOyPGuAR8NW0zqT330p6e z^I`ME2JDI=_7s+GVN@eH80%G-K%?SQ*|ex={>q@Swx~)z-p{Ci8WnAoJmhN$ZPBDp4k0Ys0Vl z$E}#QxK;Wg8tYuU&kk@aVkZYDt6kPo-@I&b8JeF%f^RfL@ue(c%i>lvb(4P<6^-n6 zM`X=U9(+u8yQ@=NbyKfhuP_Aa6sgdJ$sq2aF$wgZmK5pBaC){4oQ+lAizug^4#)mf#|0rl1-4D*Mr)UMFyN>Xt6 zw}|xhEzS;fJ)-Q`P8Rr+I0J5FJ!CMtg|eIxWedC9&gN?B*D-(msy1WiVI)u~pxufL zP5euAR?N)oEL@!P7pFhI!ny3eH~NATniAyDvp^lRYKw(Lo*lG0W4siYju0a`MMbvg zp_8{^OS#RRf76t#>0un6SD>mv!uBVV>q)jDX&t~H&oPaZ$S2xp988LTYyT$ z|7&D5w=q!nwPd*E%wq}-G%WPTrg#FRapCZxP`dxMDgK6_kOMZw|2#r+*_M;}nS~)} z3ndDC1B=Tr?x{dj3^;K|Q=vH+olG4s19SBIGdpK(piPmyhRlJkVv-Gxilq-NYw3$F zs~@;)Qw(ffGB=w;umv7MR`NzS3!8rp{p7pHdvYTr=)CWal&Usju0wAiW;Vb@l&S)% zQ>P9YLMlLR^34`E_T}qy6(261Vvo7-uQf8;shq_t@M1Oo%zfx|)FOQ)H6hos_;W3@ z>i}zpv35cQi$FYmeHuRk{(X8%5RWR~`}umU(Agw#1k>B4a_s#2L#w+h62;}^3-ETz z;^-|;kn{8A=G(6~o^ieU_52Oj?}Ww1N)lwP;YSxnuFNkoX%>lZSYA(05R96m%$}Ms znx5F{H~l~jsB@Xr#Ka%6!QM3VZBd)tDsH`sJ<)+u$N5y8garzWwoShNwWobC*_h7yGDkL{I9US3#D4mF zRBtxsetqThV}97kD_u^+5f2_zVr%SW;ojJ(4Me8A#7v>wN?kaKL)-5W|B48$0A7Qp zGykmxjS{e+&Eyjb$@+H8ZudEy#f|5Q(P*)cu5nA=f6kc|Y!XXiq^b@HDN3m73d>nO zCuM7=(KI~HPSMyCF;?qg5Fsxw<5dG`K3Xf19wc{hQ zK79B$;fm-|t5IC&)LmXi=5vSl}NFexfej7I$f&492a=tE$S6>~$pW&3EmhMjw# zp6M}Fm=2*Vj-R?kKeJW87~wayW?CQ0snL|iIhofNbrK$R`Gi@OUqYdrp>KA4dbEzss3Mau)q>whj34NANxO?Zw zWALH}%6hS2e3oRzcd!TnyI$m44MEz>xX6LH-)(%QOC` zEsq;ZfHA=>sTKBnP=n&!j^ zb35HZ=561?*WWECSb&z${+ zS-11A4egF~+}hAOt}V0N?Us#4G+q+?v7u$S)5`awaI$H*<^A_ZSqE%rdz@za=0)(W z^9euAB?QB}xVRQTtWzqZN>b=gRG?(GyLp=?J}@(OuW^W=9nPW8)NXzHMjB1O+TVy% zKcmBP>f8fH@cPz;_Uz7u7SMQix;2W*hD@75BJPK)KTWc}A;j}2{|xT9>E=!7a*f>! zcibBe6*}}SWSeHS*>mEY=@NgsNo0p@>;@pdD!7v6P#=AWnPHJI-ovYH`q8zp z%IGnTnlt1LzW!w1h&*#F?suPwyASU75Hs=JWMhuHTZ+$WZ)3`Z5~stv-iEQmGDlGf zqjlCQPJh$JxWY#CkJBcFvsx7D;jWk?q5OxE@7Q9a4nW5N}{|i3UVq5b2*v+#meX09mXas<0=XugY?3l#l8=ByCqf7kV z30{AtbsCf;(fdhF>1)Y(u7@ems<^{UI~heZar zs^jBa6`^Udo!ATrUz$p2A(Tmk{}6^Ql3TRch*86^WepRm9MhV3*zGKhDiP`6t2U7| zPv)1SVBe*LM(yYS4LEOsmiLdbxA(I~a&PdN_qy_oepTPA$)H!Rvy1rh*xA7v_G?Mo zPW+W>!m1v7xkl{MacwpY%mhELQRWVQd!~A+b@M4`-wlghVK?2GoINw|diiLcm_6ni zmd82N&6%0j&GZ$%pv_KYl!CRo8Hgk<9~xFi=)c7BJfFQxI+o2oZ_U9=9{_aq3w z=H@yHnk>KZ3n#y`!rX`xZ56oQwanM8$?6hw$96gorSwCSt&0y?fkwdMFZE>FF@{8i z$sF5@drpRD?%Z`x`<^dv?$6}X83Q1fb7#2jZOrmdlRow=Xfq;jFWpQto6@HEQTqqY zp;}WbKh0!sjxxyTQy^9R|41!n_vZ3(p6hINn(|0Nt9oeLqF6jFZOx^Br7pBQ&OKQv zlgeyZS=v2VJ2}{DhPquIOO5X60a^yORxBFp>8qb$a|r__3oh0z=xI4b` zmaF$hv6lZB-q)7ro>8gZa5Y3FZ@6ymOy--YYq^z02wNu457`~q9f|l;h{_`UXYaHk z`}Z*lSFRz=3e&|TzeJgph!GaDk@egoHbAihXClxB+QE2`;RPgRv6xq1ez(tV55)2T z;uT(O#t1i$M%wp^{RX!GA5}Kbp|9GVzP3;8Esoxk$55j99LInt;Uy$$Ip)vQrVC-A zi`%g17bK4~wG=pGG-pt^V?ZKjtTM=cLb_}$?~V)_jy0VBD48u(NuslBM_v`3QxMas z!8B7Zpurdn7am5RqT@?U>EiiW;5!-vMl-jn#(9uyY2W8@kM`qhuEuXY`|<+uaRxq9 zo%*=$$W!Q#Rpj1m`a6D-6K&3g9XH@{pjHFIp9CUzLRPk!UOP`pi}|i!-DmRm5-!B zB#0PCAkl#$+P>$dc-3Q;-hhgxwh&TOlOUrYBVp#0vcXCQ`&Yr|t24BOIE2968OX;B znEo{|n8?D#i;}K&0pA(b{qazxm(6c^|`Qd98rm8XD&pA^PFauV*a zf6WcOHe`zS_T-ky@K@-%Ty11;0I>qrl;S@-bzkG5f)FEAcFvFDzapDxHL!2CDPxD0 z#Waa=f_j^N`Zr4%2dm>>$&Lm%7{QkJG=w&^I(}D+F{fo;rUcj-*R{N!wq0Mgkw%CH zepp}FZol^e(Al`EcxzuI*;p2TbA9r(-IGyNwS=+R!WrLBerEjbYG``Ny+dpxo~*Nr zr6nmsO2R3}##9xnw`I;Rj%0o6CTOW6aaWju_ly30uWAK7UJ|HyUMB?fo&0d3tVfm3 zW!6$nht360g#}r2su0)kPsWl6kg*irTZL_Qp8`@Yw(q?1&ljcj&GRZxF710)Pl*^M z3sn{N*U1O&_Z2{)`9M`(3mbP=VvA^1ul66s+Rw>-*#!bel^;VAfWr@aLB)d15R85q z<1$hnXQRgVUgkwBCu~RZ2!q2FP}>Yyzy#4i#u2OhU&a9G8_7X7plkV~(fE?unYy>! z;l8^=_x~=kdG7iQx5)f6(JU4cU^oMC;r~oygL4EC-RNImCjh$hR#9Kbfj^hF^@VYY zO{wi$ulo*Ly5;?}lMw54zPvLZ=0=deHAWw*-?>0qTeO|o?0atuH{PrDIk|Y|uI(xHh zkTcrk)@+6fFQ6|ots{R6+QA5*hDEe;(g-4fBz!@MQr^wR;iS{KVr4%BZWF|R20lJ_ zi`VR=y~|ZPdX}uRl1?u>I}jJYuxMec<#y*av))r?Xau}w3SED^X1(-m7Qt234S!Wq z={%n;;{jtyrW`Nu{HpZEO30GxC&~VZh#u#E&-3BelBva5zwOBrFd1I^5$(_>nZSQs zchLy1kDj^f$=kr>EVs!tt7F`;si3B&CYN_ZGC;#y_ZrJ=DGEi=SW`-~}MlVzhgC+

      ChyH~yioMU z{?(YYgG+^B z_#MGKiOcP?(ZzR>2$&|aG+jP0RM+7)Cs zQ}px+O~>+eO-qe!dgM-F>7H;bv#u)7NYxqTK*sf7r&-WAzrAmru!sJL?D#F2_ynkO zw{NrrxpEc=b0B_~&^K?U*mN(1KGNgn&Phb}*jupkvnb)FZKF?|22wxhv8!P+yS~hzm*2Rtrzks^$H#t;bub`b!qQ?n$AS&!gOS z)q0uOcREp4aFHn;R$;IB`8OvP*X(u?uN~=sg(V($YN@*6#xwM->a@C_OoCOS8$8!zDvpvJUA-dXme^MClo4)CMtsZJws@=BFU7+nJ+XHsYdoy%H OuzH>rUj4xt>3;#Na+rMp diff --git a/x-pack/test/functional/es_archives/monitoring/singlecluster_three_nodes_shard_relocation_mb/data.json.gz b/x-pack/test/functional/es_archives/monitoring/singlecluster_three_nodes_shard_relocation_mb/data.json.gz index e2f6380f1010a2acf6b64e671d93a2874c0f86c9..4f66d024a65b96e9d35feae31f575f3508292901 100644 GIT binary patch delta 212613 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%0AO2`;+!ACfKf7>G@T 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

      !T<_8r_{wwjKoWnmo#4RR(M+b+sOVeLkT8d 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%0Dp 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 H8bSPjlsUsB From 72559010f7d649c5ecab7926be0420473280e8db Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 20 Jan 2022 15:19:13 -0700 Subject: [PATCH 31/43] [plugin-helpers/tests] filter out ci-stats warnings (#123510) --- .../kbn-plugin-helpers/src/integration_tests/build.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts index 65cbdaf88034c..71a3fbe603718 100644 --- a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts +++ b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts @@ -60,7 +60,12 @@ it('builds a generated plugin into a viable archive', async () => { } ); - expect(buildProc.all).toMatchInlineSnapshot(` + expect( + buildProc.all + ?.split('\n') + .filter((l) => !l.includes('failed to reach ci-stats service')) + .join('\n') + ).toMatchInlineSnapshot(` " info deleting the build and target directories info running @kbn/optimizer │ info initialized, 0 bundles cached From 735948de4dd94c0fc4d639fe44afc635572d420e Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 21 Jan 2022 00:17:38 +0000 Subject: [PATCH 32/43] chore(NA): splits types from code on @kbn/io-ts-utils (#123431) * chore(NA): splits types from code on @kbn/io-ts-utils * chore(NA): missing import keys Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .eslintrc.js | 4 -- package.json | 1 + packages/BUILD.bazel | 1 + packages/kbn-io-ts-utils/BUILD.bazel | 37 +++++++++++-------- .../deep_exact_rt/package.json | 4 -- .../iso_to_epoch_rt/package.json | 4 -- packages/kbn-io-ts-utils/json_rt/package.json | 4 -- .../kbn-io-ts-utils/merge_rt/package.json | 4 -- .../non_empty_string_rt/package.json | 4 -- packages/kbn-io-ts-utils/package.json | 1 - .../parseable_types/package.json | 4 -- .../props_to_schema/package.json | 4 -- packages/kbn-io-ts-utils/src/index.ts | 2 + .../strict_keys_rt/package.json | 4 -- .../to_boolean_rt/package.json | 4 -- .../to_json_schema/package.json | 4 -- .../kbn-io-ts-utils/to_number_rt/package.json | 4 -- .../kbn-server-route-repository/BUILD.bazel | 2 +- .../src/decode_request_params.test.ts | 2 +- .../src/decode_request_params.ts | 2 +- .../kbn-typed-react-router-config/BUILD.bazel | 2 +- .../src/create_router.test.tsx | 2 +- .../src/create_router.ts | 3 +- x-pack/plugins/apm/common/environment_rt.ts | 2 +- .../public/components/routing/home/index.tsx | 2 +- .../routing/service_detail/index.tsx | 2 +- .../register_apm_server_routes.test.ts | 2 +- .../apm_routes/register_apm_server_routes.ts | 3 +- .../apm/server/routes/backends/route.ts | 2 +- .../apm/server/routes/correlations/route.ts | 2 +- .../apm/server/routes/default_api_types.ts | 2 +- .../plugins/apm/server/routes/errors/route.ts | 3 +- .../routes/latency_distribution/route.ts | 2 +- .../routes/observability_overview/route.ts | 2 +- .../apm/server/routes/rum_client/route.ts | 2 +- .../apm/server/routes/services/route.ts | 4 +- .../settings/agent_configuration/route.ts | 2 +- .../apm/server/routes/source_maps/route.ts | 2 +- .../apm/server/routes/transactions/route.ts | 3 +- yarn.lock | 4 ++ 40 files changed, 52 insertions(+), 92 deletions(-) delete mode 100644 packages/kbn-io-ts-utils/deep_exact_rt/package.json delete mode 100644 packages/kbn-io-ts-utils/iso_to_epoch_rt/package.json delete mode 100644 packages/kbn-io-ts-utils/json_rt/package.json delete mode 100644 packages/kbn-io-ts-utils/merge_rt/package.json delete mode 100644 packages/kbn-io-ts-utils/non_empty_string_rt/package.json delete mode 100644 packages/kbn-io-ts-utils/parseable_types/package.json delete mode 100644 packages/kbn-io-ts-utils/props_to_schema/package.json delete mode 100644 packages/kbn-io-ts-utils/strict_keys_rt/package.json delete mode 100644 packages/kbn-io-ts-utils/to_boolean_rt/package.json delete mode 100644 packages/kbn-io-ts-utils/to_json_schema/package.json delete mode 100644 packages/kbn-io-ts-utils/to_number_rt/package.json diff --git a/.eslintrc.js b/.eslintrc.js index 5ee75d186eb24..f3d3e700fec30 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -226,10 +226,6 @@ const RESTRICTED_IMPORTS = [ name: 'react-use', message: 'Please use react-use/lib/{method} instead.', }, - { - name: '@kbn/io-ts-utils', - message: `Import directly from @kbn/io-ts-utils/{method} submodules`, - }, ]; module.exports = { diff --git a/package.json b/package.json index 4e7044b458a44..2dd2e411bdcfb 100644 --- a/package.json +++ b/package.json @@ -578,6 +578,7 @@ "@types/kbn__i18n": "link:bazel-bin/packages/kbn-i18n/npm_module_types", "@types/kbn__i18n-react": "link:bazel-bin/packages/kbn-i18n-react/npm_module_types", "@types/kbn__interpreter": "link:bazel-bin/packages/kbn-interpreter/npm_module_types", + "@types/kbn__io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils/npm_module_types", "@types/kbn__mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl/npm_module_types", "@types/kbn__monaco": "link:bazel-bin/packages/kbn-monaco/npm_module_types", "@types/kbn__optimizer": "link:bazel-bin/packages/kbn-optimizer/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index be4d1087bc21a..936091a3b85c2 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -97,6 +97,7 @@ filegroup( "//packages/kbn-i18n:build_types", "//packages/kbn-i18n-react:build_types", "//packages/kbn-interpreter:build_types", + "//packages/kbn-io-ts-utils:build_types", "//packages/kbn-mapbox-gl:build_types", "//packages/kbn-monaco:build_types", "//packages/kbn-optimizer:build_types", diff --git a/packages/kbn-io-ts-utils/BUILD.bazel b/packages/kbn-io-ts-utils/BUILD.bazel index dd81e8318e9d9..5ecfc0acc55e8 100644 --- a/packages/kbn-io-ts-utils/BUILD.bazel +++ b/packages/kbn-io-ts-utils/BUILD.bazel @@ -1,9 +1,10 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") PKG_BASE_NAME = "kbn-io-ts-utils" PKG_REQUIRE_NAME = "@kbn/io-ts-utils" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__io-ts-utils" SOURCE_FILES = glob( [ @@ -23,17 +24,6 @@ filegroup( NPM_MODULE_EXTRA_FILES = [ "package.json", - "deep_exact_rt/package.json", - "iso_to_epoch_rt/package.json", - "json_rt/package.json", - "merge_rt/package.json", - "non_empty_string_rt/package.json", - "parseable_types/package.json", - "props_to_schema/package.json", - "strict_keys_rt/package.json", - "to_boolean_rt/package.json", - "to_json_schema/package.json", - "to_number_rt/package.json", ] RUNTIME_DEPS = [ @@ -86,7 +76,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], + deps = RUNTIME_DEPS + [":target_node"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) @@ -105,3 +95,20 @@ filegroup( ], visibility = ["//visibility:public"], ) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-io-ts-utils/deep_exact_rt/package.json b/packages/kbn-io-ts-utils/deep_exact_rt/package.json deleted file mode 100644 index b42591a2e82d0..0000000000000 --- a/packages/kbn-io-ts-utils/deep_exact_rt/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/deep_exact_rt", - "types": "../target_types/deep_exact_rt" -} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/iso_to_epoch_rt/package.json b/packages/kbn-io-ts-utils/iso_to_epoch_rt/package.json deleted file mode 100644 index e96c50b9fbf4e..0000000000000 --- a/packages/kbn-io-ts-utils/iso_to_epoch_rt/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/iso_to_epoch_rt", - "types": "../target_types/iso_to_epoch_rt" -} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/json_rt/package.json b/packages/kbn-io-ts-utils/json_rt/package.json deleted file mode 100644 index f896827cf99a4..0000000000000 --- a/packages/kbn-io-ts-utils/json_rt/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/json_rt", - "types": "../target_types/json_rt" -} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/merge_rt/package.json b/packages/kbn-io-ts-utils/merge_rt/package.json deleted file mode 100644 index f7773688068e0..0000000000000 --- a/packages/kbn-io-ts-utils/merge_rt/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/merge_rt", - "types": "../target_types/merge_rt" -} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/non_empty_string_rt/package.json b/packages/kbn-io-ts-utils/non_empty_string_rt/package.json deleted file mode 100644 index 6348f6d728059..0000000000000 --- a/packages/kbn-io-ts-utils/non_empty_string_rt/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/non_empty_string_rt", - "types": "../target_types/non_empty_string_rt" -} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/package.json b/packages/kbn-io-ts-utils/package.json index fb1179b06bf45..2dc3532e05d96 100644 --- a/packages/kbn-io-ts-utils/package.json +++ b/packages/kbn-io-ts-utils/package.json @@ -1,7 +1,6 @@ { "name": "@kbn/io-ts-utils", "main": "./target_node/index.js", - "types": "./target_types/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", "private": true diff --git a/packages/kbn-io-ts-utils/parseable_types/package.json b/packages/kbn-io-ts-utils/parseable_types/package.json deleted file mode 100644 index 6dab2a5ee156e..0000000000000 --- a/packages/kbn-io-ts-utils/parseable_types/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/parseable_types", - "types": "../target_types/parseable_types" -} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/props_to_schema/package.json b/packages/kbn-io-ts-utils/props_to_schema/package.json deleted file mode 100644 index 478de84d17f81..0000000000000 --- a/packages/kbn-io-ts-utils/props_to_schema/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/props_to_schema", - "types": "../target_types/props_to_schema" -} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/src/index.ts b/packages/kbn-io-ts-utils/src/index.ts index 88cfc063f738a..a01e6c41d79d6 100644 --- a/packages/kbn-io-ts-utils/src/index.ts +++ b/packages/kbn-io-ts-utils/src/index.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +export type { NonEmptyStringBrand } from './non_empty_string_rt'; + export { deepExactRt } from './deep_exact_rt'; export { jsonRt } from './json_rt'; export { mergeRt } from './merge_rt'; diff --git a/packages/kbn-io-ts-utils/strict_keys_rt/package.json b/packages/kbn-io-ts-utils/strict_keys_rt/package.json deleted file mode 100644 index 68823d97a5d00..0000000000000 --- a/packages/kbn-io-ts-utils/strict_keys_rt/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/strict_keys_rt", - "types": "../target_types/strict_keys_rt" -} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/to_boolean_rt/package.json b/packages/kbn-io-ts-utils/to_boolean_rt/package.json deleted file mode 100644 index 5e801a6529153..0000000000000 --- a/packages/kbn-io-ts-utils/to_boolean_rt/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/to_boolean_rt", - "types": "../target_types/to_boolean_rt" -} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/to_json_schema/package.json b/packages/kbn-io-ts-utils/to_json_schema/package.json deleted file mode 100644 index 366f3243b1156..0000000000000 --- a/packages/kbn-io-ts-utils/to_json_schema/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/to_json_schema", - "types": "../target_types/to_json_schema" -} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/to_number_rt/package.json b/packages/kbn-io-ts-utils/to_number_rt/package.json deleted file mode 100644 index f5da955cb9775..0000000000000 --- a/packages/kbn-io-ts-utils/to_number_rt/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/to_number_rt", - "types": "../target_types/to_number_rt" -} \ No newline at end of file diff --git a/packages/kbn-server-route-repository/BUILD.bazel b/packages/kbn-server-route-repository/BUILD.bazel index 103f15bbf5d6a..a0e1cf41dcf8f 100644 --- a/packages/kbn-server-route-repository/BUILD.bazel +++ b/packages/kbn-server-route-repository/BUILD.bazel @@ -38,7 +38,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/kbn-config-schema:npm_module_types", - "//packages/kbn-io-ts-utils", + "//packages/kbn-io-ts-utils:npm_module_types", "@npm//@hapi/boom", "@npm//fp-ts", "@npm//utility-types", diff --git a/packages/kbn-server-route-repository/src/decode_request_params.test.ts b/packages/kbn-server-route-repository/src/decode_request_params.test.ts index a5c1a2b49eb19..96035bc3e0011 100644 --- a/packages/kbn-server-route-repository/src/decode_request_params.test.ts +++ b/packages/kbn-server-route-repository/src/decode_request_params.test.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { jsonRt } from '@kbn/io-ts-utils/json_rt'; +import { jsonRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { decodeRequestParams } from './decode_request_params'; diff --git a/packages/kbn-server-route-repository/src/decode_request_params.ts b/packages/kbn-server-route-repository/src/decode_request_params.ts index 4df6fa3333c50..00492d69b8ac5 100644 --- a/packages/kbn-server-route-repository/src/decode_request_params.ts +++ b/packages/kbn-server-route-repository/src/decode_request_params.ts @@ -10,7 +10,7 @@ import { omitBy, isPlainObject, isEmpty } from 'lodash'; import { isLeft } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; import Boom from '@hapi/boom'; -import { strictKeysRt } from '@kbn/io-ts-utils/strict_keys_rt'; +import { strictKeysRt } from '@kbn/io-ts-utils'; import { RouteParamsRT } from './typings'; interface KibanaRequestParams { diff --git a/packages/kbn-typed-react-router-config/BUILD.bazel b/packages/kbn-typed-react-router-config/BUILD.bazel index d759948a6c576..6f4e53e58fff7 100644 --- a/packages/kbn-typed-react-router-config/BUILD.bazel +++ b/packages/kbn-typed-react-router-config/BUILD.bazel @@ -37,7 +37,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-io-ts-utils", + "//packages/kbn-io-ts-utils:npm_module_types", "@npm//query-string", "@npm//utility-types", "@npm//@types/jest", diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index ac337f8bb5b87..9837d45ddd869 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; import * as t from 'io-ts'; -import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; +import { toNumberRt } from '@kbn/io-ts-utils'; import { createRouter } from './create_router'; import { createMemoryHistory } from 'history'; import { route } from './route'; diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index 89ff4fc6b0c6c..27a4f1dd5de02 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -15,8 +15,7 @@ import { } from 'react-router-config'; import qs from 'query-string'; import { findLastIndex, merge, compact } from 'lodash'; -import { mergeRt } from '@kbn/io-ts-utils/merge_rt'; -import { deepExactRt } from '@kbn/io-ts-utils/deep_exact_rt'; +import { deepExactRt, mergeRt } from '@kbn/io-ts-utils'; import { FlattenRoutesOf, Route, Router } from './types'; function toReactRouterPath(path: string) { diff --git a/x-pack/plugins/apm/common/environment_rt.ts b/x-pack/plugins/apm/common/environment_rt.ts index 67d1a6ce6fa64..dd07bb9f47318 100644 --- a/x-pack/plugins/apm/common/environment_rt.ts +++ b/x-pack/plugins/apm/common/environment_rt.ts @@ -5,7 +5,7 @@ * 2.0. */ import * as t from 'io-ts'; -import { nonEmptyStringRt } from '@kbn/io-ts-utils/non_empty_string_rt'; +import { nonEmptyStringRt } from '@kbn/io-ts-utils'; import { ENVIRONMENT_ALL, ENVIRONMENT_NOT_DEFINED, diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index e70cb31eef88f..25a68592d2b11 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { Outlet } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React from 'react'; -import { toBooleanRt } from '@kbn/io-ts-utils/to_boolean_rt'; +import { toBooleanRt } from '@kbn/io-ts-utils'; import { RedirectTo } from '../redirect_to'; import { comparisonTypeRt } from '../../../../common/runtime_types/comparison_type_rt'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index 713292c633891..f8206f273abd4 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { Outlet } from '@kbn/typed-react-router-config'; -import { toBooleanRt } from '@kbn/io-ts-utils/to_boolean_rt'; +import { toBooleanRt } from '@kbn/io-ts-utils'; import { comparisonTypeRt } from '../../../../common/runtime_types/comparison_type_rt'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { environmentRt } from '../../../../common/environment_rt'; diff --git a/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.test.ts b/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.test.ts index 371652cdab957..c2391967f1cd2 100644 --- a/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.test.ts +++ b/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { jsonRt } from '@kbn/io-ts-utils/json_rt'; +import { jsonRt } from '@kbn/io-ts-utils'; import { createServerRouteRepository } from '@kbn/server-route-repository'; import { ServerRoute } from '@kbn/server-route-repository'; import * as t from 'io-ts'; diff --git a/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.ts b/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.ts index a7a4356923655..e84f97c70691b 100644 --- a/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.ts +++ b/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.ts @@ -17,8 +17,7 @@ import { parseEndpoint, routeValidationObject, } from '@kbn/server-route-repository'; -import { mergeRt } from '@kbn/io-ts-utils/merge_rt'; -import { jsonRt } from '@kbn/io-ts-utils/json_rt'; +import { jsonRt, mergeRt } from '@kbn/io-ts-utils'; import { pickKeys } from '../../../common/utils/pick_keys'; import { APMRouteHandlerResources, TelemetryUsageCounter } from '../typings'; import type { ApmPluginRequestHandlerContext } from '../typings'; diff --git a/x-pack/plugins/apm/server/routes/backends/route.ts b/x-pack/plugins/apm/server/routes/backends/route.ts index 58160477994bd..4f8d404fc529f 100644 --- a/x-pack/plugins/apm/server/routes/backends/route.ts +++ b/x-pack/plugins/apm/server/routes/backends/route.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; +import { toNumberRt } from '@kbn/io-ts-utils'; import { setupRequest } from '../../lib/helpers/setup_request'; import { environmentRt, diff --git a/x-pack/plugins/apm/server/routes/correlations/route.ts b/x-pack/plugins/apm/server/routes/correlations/route.ts index 377fedf9d1813..af267cd7294eb 100644 --- a/x-pack/plugins/apm/server/routes/correlations/route.ts +++ b/x-pack/plugins/apm/server/routes/correlations/route.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; -import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; +import { toNumberRt } from '@kbn/io-ts-utils'; import { isActivePlatinumLicense } from '../../../common/license_check'; diff --git a/x-pack/plugins/apm/server/routes/default_api_types.ts b/x-pack/plugins/apm/server/routes/default_api_types.ts index b31de8e53dad2..5622b12e1b099 100644 --- a/x-pack/plugins/apm/server/routes/default_api_types.ts +++ b/x-pack/plugins/apm/server/routes/default_api_types.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { isoToEpochRt } from '@kbn/io-ts-utils/iso_to_epoch_rt'; +import { isoToEpochRt } from '@kbn/io-ts-utils'; export { environmentRt } from '../../common/environment_rt'; diff --git a/x-pack/plugins/apm/server/routes/errors/route.ts b/x-pack/plugins/apm/server/routes/errors/route.ts index f4e5ac172d5b0..2ead97f74d373 100644 --- a/x-pack/plugins/apm/server/routes/errors/route.ts +++ b/x-pack/plugins/apm/server/routes/errors/route.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; -import { jsonRt } from '@kbn/io-ts-utils/json_rt'; +import { jsonRt, toNumberRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { getErrorDistribution } from './distribution/get_distribution'; diff --git a/x-pack/plugins/apm/server/routes/latency_distribution/route.ts b/x-pack/plugins/apm/server/routes/latency_distribution/route.ts index f30e98dd8c7b8..675429df9df3f 100644 --- a/x-pack/plugins/apm/server/routes/latency_distribution/route.ts +++ b/x-pack/plugins/apm/server/routes/latency_distribution/route.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; +import { toNumberRt } from '@kbn/io-ts-utils'; import { getOverallLatencyDistribution } from './get_overall_latency_distribution'; import { setupRequest } from '../../lib/helpers/setup_request'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; diff --git a/x-pack/plugins/apm/server/routes/observability_overview/route.ts b/x-pack/plugins/apm/server/routes/observability_overview/route.ts index c99586638c3de..b94fcf2f32a02 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview/route.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview/route.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; +import { toNumberRt } from '@kbn/io-ts-utils'; import { setupRequest } from '../../lib/helpers/setup_request'; import { getServiceCount } from './get_service_count'; import { getTransactionsPerMinute } from './get_transactions_per_minute'; diff --git a/x-pack/plugins/apm/server/routes/rum_client/route.ts b/x-pack/plugins/apm/server/routes/rum_client/route.ts index 482dcc0799ed0..1b1b87f5d1198 100644 --- a/x-pack/plugins/apm/server/routes/rum_client/route.ts +++ b/x-pack/plugins/apm/server/routes/rum_client/route.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; import { Logger } from 'kibana/server'; -import { isoToEpochRt } from '@kbn/io-ts-utils/iso_to_epoch_rt'; +import { isoToEpochRt } from '@kbn/io-ts-utils'; import { setupRequest, Setup } from '../../lib/helpers/setup_request'; import { getClientMetrics } from './get_client_metrics'; import { getJSErrors } from './get_js_errors'; diff --git a/x-pack/plugins/apm/server/routes/services/route.ts b/x-pack/plugins/apm/server/routes/services/route.ts index 2b7a71ba13acf..de5f457f18dec 100644 --- a/x-pack/plugins/apm/server/routes/services/route.ts +++ b/x-pack/plugins/apm/server/routes/services/route.ts @@ -6,9 +6,7 @@ */ import Boom from '@hapi/boom'; -import { jsonRt } from '@kbn/io-ts-utils/json_rt'; -import { isoToEpochRt } from '@kbn/io-ts-utils/iso_to_epoch_rt'; -import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; +import { isoToEpochRt, jsonRt, toNumberRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { uniq } from 'lodash'; import { latencyAggregationTypeRt } from '../../../common/latency_aggregation_types'; diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts index 26a9eaefc0413..b8245a8efaa94 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import Boom from '@hapi/boom'; -import { toBooleanRt } from '@kbn/io-ts-utils/to_boolean_rt'; +import { toBooleanRt } from '@kbn/io-ts-utils'; import { maxSuggestions } from '../../../../../observability/common'; import { setupRequest } from '../../../lib/helpers/setup_request'; import { getServiceNames } from './get_service_names'; diff --git a/x-pack/plugins/apm/server/routes/source_maps/route.ts b/x-pack/plugins/apm/server/routes/source_maps/route.ts index b0b7eb5134fcf..a60c59c03589d 100644 --- a/x-pack/plugins/apm/server/routes/source_maps/route.ts +++ b/x-pack/plugins/apm/server/routes/source_maps/route.ts @@ -7,7 +7,7 @@ import Boom from '@hapi/boom'; import * as t from 'io-ts'; import { SavedObjectsClientContract } from 'kibana/server'; -import { jsonRt } from '@kbn/io-ts-utils/json_rt'; +import { jsonRt } from '@kbn/io-ts-utils'; import { createApmArtifact, deleteApmArtifact, diff --git a/x-pack/plugins/apm/server/routes/transactions/route.ts b/x-pack/plugins/apm/server/routes/transactions/route.ts index cad1c3b8f353b..49c50f5a9100d 100644 --- a/x-pack/plugins/apm/server/routes/transactions/route.ts +++ b/x-pack/plugins/apm/server/routes/transactions/route.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { jsonRt } from '@kbn/io-ts-utils/json_rt'; -import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; +import { jsonRt, toNumberRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { LatencyAggregationType, diff --git a/yarn.lock b/yarn.lock index 2e4d6ddc987fb..f840945352c91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5864,6 +5864,10 @@ version "0.0.0" uid "" +"@types/kbn__io-ts-utils@link:bazel-bin/packages/kbn-io-ts-utils/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__mapbox-gl@link:bazel-bin/packages/kbn-mapbox-gl/npm_module_types": version "0.0.0" uid "" From 48693d1fd6d558465e93995328282e0788eb1319 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Thu, 20 Jan 2022 18:47:36 -0700 Subject: [PATCH 33/43] [RAC][Alerting][Security Solution] Adds Rule Execution UUID (#113058) ## Summary Resolves: https://github.com/elastic/kibana/issues/110135 This PR is for introducing a new UUID (`kibana.alert.rule.execution.uuid` as defined in the AAD schema) for identifying individual rule executions. This id is introduced as a `private readonly` member of the [alerting server task_manager](https://github.com/elastic/kibana/blob/a993668663dd4fc25d3336e2d474101ed8d1b74d/x-pack/plugins/alerting/server/task_runner/task_runner.ts#L123), and plumbed through the `executionHandler` and to all appropriate alert event and event-log touch points. For persistence when writing alerts within the RuleRegistry, `kibana.alert.rule.execution.uuid` is plumbed through [`getCommonAlertFields()`](https://github.com/elastic/kibana/blob/c81341c325edcb0eaca1dab2521b2a86fea18389/x-pack/plugins/rule_registry/server/utils/get_common_alert_fields.ts#L52) so it is grouped with like fields and is picked up by both the [`createPersistenceRuleTypeWrapper`](https://github.com/elastic/kibana/blob/c81341c325edcb0eaca1dab2521b2a86fea18389/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts#L38) used by Security Solution, and [`createLifecycleExecutor`](https://github.com/elastic/kibana/blob/d152ca5b6bf7f56fcba1d1d8c2cfee5404a821de/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts#L157) used by Observability rules. Additionally on the Security Solution side, `kibana.alert.rule.execution.uuid` was plumbed through the `RuleExecutionLog` so that all events written to the event-log will now include this id so individual rule status events/metrics can be correlated with specific rule executions. No UI facing changes were made, however `kibana.alert.rule.execution.uuid` is now available within the Alerts Table FieldBrowser, and can be toggled and viewed alongside alerts:

      As visible when exploring `event-log` in Discover:

      ### Checklist Delete any items that are not applicable to this PR. - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - Will need to sync with Doc folks on updates here. - [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 --- .../src/technical_field_names.ts | 3 + ...eate_alert_event_log_record_object.test.ts | 24 + .../create_alert_event_log_record_object.ts | 25 +- .../server/rules_client/tests/disable.test.ts | 4 + .../create_execution_handler.test.ts | 8 + .../task_runner/create_execution_handler.ts | 3 + .../server/task_runner/task_runner.test.ts | 445 ++++++++++++++++++ .../server/task_runner/task_runner.ts | 23 + .../task_runner/task_runner_cancel.test.ts | 88 ++++ x-pack/plugins/alerting/server/types.ts | 1 + .../metric_threshold_executor.test.ts | 1 + .../field_maps/technical_rule_field_map.ts | 5 + .../utils/create_lifecycle_rule_type.test.ts | 3 + .../server/utils/get_common_alert_fields.ts | 3 + .../server/utils/rule_executor_test_utils.ts | 1 + .../detection_alerts/alerts_details.spec.ts | 2 +- ...gacy_rules_notification_alert_type.test.ts | 1 + .../get_signals_template.test.ts.snap | 7 + .../routes/index/signals_mapping.json | 7 + .../routes/rules/preview_rules_route.ts | 1 + .../__mocks__/rule_execution_log_client.ts | 1 + .../rule_execution_events/events_writer.ts | 7 +- .../rule_execution_logger/logger.ts | 8 +- .../rule_execution_logger/logger_interface.ts | 1 + .../create_security_rule_type_wrapper.ts | 3 + .../factories/build_rule_message_factory.ts | 4 +- .../rule_types/factories/utils/build_alert.ts | 2 + .../detection_engine/signals/utils.test.ts | 1 - .../alert_types/es_query/alert_type.test.ts | 6 + .../index_threshold/alert_type.test.ts | 3 + .../common/lib/get_event_log.ts | 6 +- .../tests/alerting/event_log_alerts.ts | 19 + .../security_and_spaces/tests/create_ml.ts | 2 + .../spaces_only/tests/trial/create_rule.ts | 10 +- 34 files changed, 716 insertions(+), 12 deletions(-) diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index c6edd30549a76..e49b47c712780 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -47,6 +47,7 @@ const ALERT_RULE_CREATED_AT = `${ALERT_RULE_NAMESPACE}.created_at` as const; const ALERT_RULE_CREATED_BY = `${ALERT_RULE_NAMESPACE}.created_by` as const; const ALERT_RULE_DESCRIPTION = `${ALERT_RULE_NAMESPACE}.description` as const; const ALERT_RULE_ENABLED = `${ALERT_RULE_NAMESPACE}.enabled` as const; +const ALERT_RULE_EXECUTION_UUID = `${ALERT_RULE_NAMESPACE}.execution.uuid` as const; const ALERT_RULE_FROM = `${ALERT_RULE_NAMESPACE}.from` as const; const ALERT_RULE_INTERVAL = `${ALERT_RULE_NAMESPACE}.interval` as const; const ALERT_RULE_LICENSE = `${ALERT_RULE_NAMESPACE}.license` as const; @@ -103,6 +104,7 @@ const fields = { ALERT_RULE_CREATED_BY, ALERT_RULE_DESCRIPTION, ALERT_RULE_ENABLED, + ALERT_RULE_EXECUTION_UUID, ALERT_RULE_FROM, ALERT_RULE_INTERVAL, ALERT_RULE_LICENSE, @@ -156,6 +158,7 @@ export { ALERT_RULE_CREATED_BY, ALERT_RULE_DESCRIPTION, ALERT_RULE_ENABLED, + ALERT_RULE_EXECUTION_UUID, ALERT_RULE_FROM, ALERT_RULE_INTERVAL, ALERT_RULE_LICENSE, diff --git a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts index a7a00034e7064..3895c90d4a6c2 100644 --- a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts +++ b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts @@ -25,6 +25,7 @@ describe('createAlertEventLogRecordObject', () => { test('created alert event "execute-start"', async () => { expect( createAlertEventLogRecordObject({ + executionId: '7a7065d7-6e8b-4aae-8d20-c93613dec9fb', ruleId: '1', ruleType, action: 'execute-start', @@ -50,6 +51,13 @@ describe('createAlertEventLogRecordObject', () => { kind: 'alert', }, kibana: { + alert: { + rule: { + execution: { + uuid: '7a7065d7-6e8b-4aae-8d20-c93613dec9fb', + }, + }, + }, saved_objects: [ { id: '1', @@ -76,6 +84,7 @@ describe('createAlertEventLogRecordObject', () => { test('created alert event "recovered-instance"', async () => { expect( createAlertEventLogRecordObject({ + executionId: '7a7065d7-6e8b-4aae-8d20-c93613dec9fb', ruleId: '1', ruleName: 'test name', ruleType, @@ -109,6 +118,13 @@ describe('createAlertEventLogRecordObject', () => { start: '1970-01-01T00:00:00.000Z', }, kibana: { + alert: { + rule: { + execution: { + uuid: '7a7065d7-6e8b-4aae-8d20-c93613dec9fb', + }, + }, + }, alerting: { action_group_id: 'group 1', action_subgroup: 'subgroup value', @@ -138,6 +154,7 @@ describe('createAlertEventLogRecordObject', () => { test('created alert event "execute-action"', async () => { expect( createAlertEventLogRecordObject({ + executionId: '7a7065d7-6e8b-4aae-8d20-c93613dec9fb', ruleId: '1', ruleName: 'test name', ruleType, @@ -176,6 +193,13 @@ describe('createAlertEventLogRecordObject', () => { start: '1970-01-01T00:00:00.000Z', }, kibana: { + alert: { + rule: { + execution: { + uuid: '7a7065d7-6e8b-4aae-8d20-c93613dec9fb', + }, + }, + }, alerting: { action_group_id: 'group 1', action_subgroup: 'subgroup value', diff --git a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts index e06b5bf893bac..95e33d394fbd2 100644 --- a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts +++ b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts @@ -12,6 +12,7 @@ import { UntypedNormalizedRuleType } from '../rule_type_registry'; export type Event = Exclude; interface CreateAlertEventLogRecordParams { + executionId?: string; ruleId: string; ruleType: UntypedNormalizedRuleType; action: string; @@ -36,7 +37,18 @@ interface CreateAlertEventLogRecordParams { } export function createAlertEventLogRecordObject(params: CreateAlertEventLogRecordParams): Event { - const { ruleType, action, state, message, task, ruleId, group, subgroup, namespace } = params; + const { + executionId, + ruleType, + action, + state, + message, + task, + ruleId, + group, + subgroup, + namespace, + } = params; const alerting = params.instanceId || group || subgroup ? { @@ -59,6 +71,17 @@ export function createAlertEventLogRecordObject(params: CreateAlertEventLogRecor }, kibana: { ...(alerting ? alerting : {}), + ...(executionId + ? { + alert: { + rule: { + execution: { + uuid: executionId, + }, + }, + }, + } + : {}), saved_objects: params.savedObjects.map((so) => ({ ...(so.relation ? { rel: so.relation } : {}), type: so.type, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts index a5b9f1d928e81..9c3e5872c76e1 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts @@ -21,6 +21,10 @@ import { getBeforeSetup, setGlobalDate } from './lib'; import { eventLoggerMock } from '../../../../event_log/server/event_logger.mock'; import { TaskStatus } from '../../../../task_manager/server'; +jest.mock('uuid', () => ({ + v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', +})); + const taskManager = taskManagerMock.createStart(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 69b094585d703..71ec12e29a9dd 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -73,6 +73,7 @@ const createExecutionHandlerParams: jest.Mocked< spaceId: 'test1', ruleId: '1', ruleName: 'name-of-alert', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', tags: ['tag-A', 'tag-B'], apiKey: 'MTIzOmFiYw==', kibanaBaseUrl: 'http://localhost:5601', @@ -173,6 +174,13 @@ test('enqueues execution per selected action', async () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "2", diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 112cb949e3ad7..58f8089890c87 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -36,6 +36,7 @@ export interface CreateExecutionHandlerOptions< > { ruleId: string; ruleName: string; + executionId: string; tags?: string[]; actionsPlugin: ActionsPluginStartContract; actions: AlertAction[]; @@ -83,6 +84,7 @@ export function createExecutionHandler< logger, ruleId, ruleName, + executionId, tags, actionsPlugin, actions: ruleActions, @@ -206,6 +208,7 @@ export function createExecutionHandler< ruleId, ruleType: ruleType as UntypedNormalizedRuleType, action: EVENT_LOG_ACTIONS.executeAction, + executionId, instanceId: alertId, group: actionGroup, subgroup: actionSubgroup, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 61d41e674c209..a466583cd3bd3 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -41,6 +41,10 @@ import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; import { ExecuteOptions } from '../../../actions/server/create_execute_function'; +jest.mock('uuid', () => ({ + v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', +})); + const ruleType: jest.Mocked = { id: 'test', name: 'My test rule', @@ -323,6 +327,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -484,6 +495,13 @@ describe('Task Runner', () => { kind: 'alert', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, task: { schedule_delay: 0, scheduled: '1970-01-01T00:00:00.000Z', @@ -515,6 +533,13 @@ describe('Task Runner', () => { start: '1970-01-01T00:00:00.000Z', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, alerting: { action_group_id: 'default', action_subgroup: 'subDefault', @@ -549,6 +574,13 @@ describe('Task Runner', () => { start: '1970-01-01T00:00:00.000Z', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, alerting: { action_group_id: 'default', action_subgroup: 'subDefault', @@ -576,6 +608,13 @@ describe('Task Runner', () => { kind: 'alert', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, alerting: { instance_id: '1', action_group_id: 'default', @@ -611,6 +650,13 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(5, { event: { action: 'execute', category: ['alerts'], kind: 'alert', outcome: 'success' }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, alerting: { status: 'active', }, @@ -707,6 +753,13 @@ describe('Task Runner', () => { schedule_delay: 0, scheduled: '1970-01-01T00:00:00.000Z', }, + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, saved_objects: [ { id: '1', @@ -734,6 +787,13 @@ describe('Task Runner', () => { start: '1970-01-01T00:00:00.000Z', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, alerting: { action_group_id: 'default', instance_id: '1', @@ -767,6 +827,13 @@ describe('Task Runner', () => { start: '1970-01-01T00:00:00.000Z', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, alerting: { instance_id: '1', action_group_id: 'default', @@ -799,6 +866,13 @@ describe('Task Runner', () => { outcome: 'success', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, alerting: { status: 'active', }, @@ -963,6 +1037,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -998,6 +1079,13 @@ describe('Task Runner', () => { "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "1", @@ -1033,6 +1121,13 @@ describe('Task Runner', () => { "outcome": "success", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "active", }, @@ -1302,6 +1397,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -1337,6 +1439,13 @@ describe('Task Runner', () => { "start": "1970-01-01T00:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "1", @@ -1373,6 +1482,13 @@ describe('Task Runner', () => { "start": "1970-01-01T00:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "1", @@ -1407,6 +1523,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "1", @@ -1448,6 +1571,13 @@ describe('Task Runner', () => { "outcome": "success", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "active", }, @@ -1597,6 +1727,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -1633,6 +1770,13 @@ describe('Task Runner', () => { "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "instance_id": "2", }, @@ -1668,6 +1812,13 @@ describe('Task Runner', () => { "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "1", @@ -1702,6 +1853,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "recovered", "instance_id": "2", @@ -1742,6 +1900,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "1", @@ -1783,6 +1948,13 @@ describe('Task Runner', () => { "outcome": "success", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "active", }, @@ -2165,6 +2337,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -2201,6 +2380,13 @@ describe('Task Runner', () => { "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "2", @@ -2237,6 +2423,13 @@ describe('Task Runner', () => { "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "1", @@ -2272,6 +2465,13 @@ describe('Task Runner', () => { "outcome": "success", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "active", }, @@ -2546,6 +2746,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -2584,6 +2791,13 @@ describe('Task Runner', () => { "reason": "execute", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "error", }, @@ -2666,6 +2880,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -2704,6 +2925,13 @@ describe('Task Runner', () => { "reason": "decrypt", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "error", }, @@ -2795,6 +3023,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -2833,6 +3068,13 @@ describe('Task Runner', () => { "reason": "license", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "error", }, @@ -2924,6 +3166,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -2962,6 +3211,13 @@ describe('Task Runner', () => { "reason": "unknown", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "error", }, @@ -3052,6 +3308,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -3090,6 +3353,13 @@ describe('Task Runner', () => { "reason": "read", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "error", }, @@ -3358,6 +3628,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -3393,6 +3670,13 @@ describe('Task Runner', () => { "start": "1970-01-01T00:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "1", @@ -3429,6 +3713,13 @@ describe('Task Runner', () => { "start": "1970-01-01T00:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "2", @@ -3465,6 +3756,13 @@ describe('Task Runner', () => { "start": "1970-01-01T00:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "1", @@ -3501,6 +3799,13 @@ describe('Task Runner', () => { "start": "1970-01-01T00:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "2", @@ -3536,6 +3841,13 @@ describe('Task Runner', () => { "outcome": "success", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "active", }, @@ -3643,6 +3955,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -3678,6 +3997,13 @@ describe('Task Runner', () => { "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "1", @@ -3714,6 +4040,13 @@ describe('Task Runner', () => { "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "2", @@ -3749,6 +4082,13 @@ describe('Task Runner', () => { "outcome": "success", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "active", }, @@ -3848,6 +4188,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -3881,6 +4228,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "1", @@ -3915,6 +4269,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "action_group_id": "default", "instance_id": "2", @@ -3950,6 +4311,13 @@ describe('Task Runner', () => { "outcome": "success", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "active", }, @@ -4044,6 +4412,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -4080,6 +4455,13 @@ describe('Task Runner', () => { "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "instance_id": "1", }, @@ -4116,6 +4498,13 @@ describe('Task Runner', () => { "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "instance_id": "2", }, @@ -4150,6 +4539,13 @@ describe('Task Runner', () => { "outcome": "success", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "ok", }, @@ -4246,6 +4642,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -4279,6 +4682,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "instance_id": "1", }, @@ -4312,6 +4722,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "instance_id": "2", }, @@ -4346,6 +4763,13 @@ describe('Task Runner', () => { "outcome": "success", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "alerting": Object { "status": "ok", }, @@ -4506,6 +4930,13 @@ describe('Task Runner', () => { "kind": "alert", }, "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + }, + }, + }, "saved_objects": Array [ Object { "id": "1", @@ -4596,6 +5027,13 @@ describe('Task Runner', () => { category: ['alerts'], }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, saved_objects: [ { rel: 'primary', type: 'alert', id: '1', namespace: undefined, type_id: 'test' }, ], @@ -4618,6 +5056,13 @@ describe('Task Runner', () => { outcome: 'failure', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, saved_objects: [ { rel: 'primary', type: 'alert', id: '1', namespace: undefined, type_id: 'test' }, ], diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index b4fa5a1927fee..9640dd9038ce7 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -8,6 +8,7 @@ import apm from 'elastic-apm-node'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { Dictionary, pickBy, mapValues, without, cloneDeep } from 'lodash'; import type { Request } from '@hapi/hapi'; +import uuid from 'uuid'; import { addSpaceIdToPath } from '../../../spaces/server'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; @@ -106,6 +107,7 @@ export class TaskRunner< ActionGroupIds, RecoveryActionGroupId >; + private readonly executionId: string; private readonly ruleTypeRegistry: RuleTypeRegistry; private searchAbortController: AbortController; private cancelled: boolean; @@ -131,6 +133,7 @@ export class TaskRunner< this.ruleTypeRegistry = context.ruleTypeRegistry; this.searchAbortController = new AbortController(); this.cancelled = false; + this.executionId = uuid.v4(); } async getDecryptedAttributes( @@ -209,6 +212,7 @@ export class TaskRunner< ruleId, ruleName, tags, + executionId: this.executionId, logger: this.logger, actionsPlugin: this.context.actionsPlugin, apiKey, @@ -324,6 +328,7 @@ export class TaskRunner< updatedRuleTypeState = await this.context.executionContext.withContext(ctx, () => this.ruleType.executor({ alertId: ruleId, + executionId: this.executionId, services: { ...services, alertInstanceFactory: createAlertInstanceFactory< @@ -411,6 +416,7 @@ export class TaskRunner< if (this.shouldLogAndScheduleActionsForAlerts()) { generateNewAndRecoveredAlertEvents({ eventLogger, + executionId: this.executionId, originalAlerts, currentAlerts: alertsWithScheduledActions, recoveredAlerts, @@ -612,6 +618,7 @@ export class TaskRunner< ruleType: this.ruleType as UntypedNormalizedRuleType, action: EVENT_LOG_ACTIONS.execute, namespace, + executionId: this.executionId, task: { scheduled: this.taskInstance.runAt.toISOString(), scheduleDelay: Millis2Nanos * scheduleDelay, @@ -785,6 +792,13 @@ export class TaskRunner< this.ruleType.ruleTaskTimeout }`, kibana: { + alert: { + rule: { + execution: { + uuid: this.executionId, + }, + }, + }, saved_objects: [ { rel: SAVED_OBJECT_REL_PRIMARY, @@ -883,6 +897,7 @@ interface GenerateNewAndRecoveredAlertEventsParams< InstanceContext extends AlertInstanceContext > { eventLogger: IEventLogger; + executionId: string; originalAlerts: Dictionary>; currentAlerts: Dictionary>; recoveredAlerts: Dictionary>; @@ -911,6 +926,7 @@ function generateNewAndRecoveredAlertEvents< >(params: GenerateNewAndRecoveredAlertEventsParams) { const { eventLogger, + executionId, ruleId, namespace, currentAlerts, @@ -990,6 +1006,13 @@ function generateNewAndRecoveredAlertEvents< ...(state?.duration !== undefined ? { duration: state.duration as number } : {}), }, kibana: { + alert: { + rule: { + execution: { + uuid: executionId, + }, + }, + }, alerting: { instance_id: alertId, ...(group ? { action_group_id: group } : {}), diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 3e3c3351a8e67..e24be639c7fcc 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -32,6 +32,10 @@ import { Alert, RecoveredActionGroup } from '../../common'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; +jest.mock('uuid', () => ({ + v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', +})); + const ruleType: jest.Mocked = { id: 'test', name: 'My test rule', @@ -208,6 +212,13 @@ describe('Task Runner Cancel', () => { kind: 'alert', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, saved_objects: [ { id: '1', @@ -236,6 +247,13 @@ describe('Task Runner Cancel', () => { kind: 'alert', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, saved_objects: [ { id: '1', @@ -261,6 +279,13 @@ describe('Task Runner Cancel', () => { outcome: 'success', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, alerting: { status: 'ok', }, @@ -437,6 +462,13 @@ describe('Task Runner Cancel', () => { kind: 'alert', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, task: { schedule_delay: 0, scheduled: '1970-01-01T00:00:00.000Z', @@ -465,6 +497,13 @@ describe('Task Runner Cancel', () => { kind: 'alert', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, saved_objects: [ { id: '1', @@ -491,6 +530,13 @@ describe('Task Runner Cancel', () => { outcome: 'success', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, alerting: { status: 'active', }, @@ -553,6 +599,13 @@ describe('Task Runner Cancel', () => { kind: 'alert', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, task: { schedule_delay: 0, scheduled: '1970-01-01T00:00:00.000Z', @@ -582,6 +635,13 @@ describe('Task Runner Cancel', () => { kind: 'alert', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, saved_objects: [ { id: '1', @@ -609,6 +669,13 @@ describe('Task Runner Cancel', () => { start: '1970-01-01T00:00:00.000Z', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, alerting: { action_group_id: 'default', instance_id: '1', @@ -642,6 +709,13 @@ describe('Task Runner Cancel', () => { start: '1970-01-01T00:00:00.000Z', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, alerting: { action_group_id: 'default', instance_id: '1', @@ -666,6 +740,13 @@ describe('Task Runner Cancel', () => { kind: 'alert', }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, alerting: { instance_id: '1', action_group_id: 'default', @@ -697,6 +778,13 @@ describe('Task Runner Cancel', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(6, { event: { action: 'execute', category: ['alerts'], kind: 'alert', outcome: 'success' }, kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + }, + }, alerting: { status: 'active', }, diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 866c8665ddb65..93ee520c7126a 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -90,6 +90,7 @@ export interface AlertExecutorOptions< ActionGroupIds extends string = never > { alertId: string; + executionId: string; startedAt: Date; previousStartedAt: Date | null; services: AlertServices; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 4bbbb355d573e..a8289d5766cc5 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -51,6 +51,7 @@ const initialRuleState: TestRuleState = { const mockOptions = { alertId: '', + executionId: '', startedAt: new Date(), previousStartedAt: null, state: { diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts index d0feac5f8aa32..c81329baad572 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -107,6 +107,11 @@ export const technicalRuleFieldMap = { array: false, required: false, }, + [Fields.ALERT_RULE_EXECUTION_UUID]: { + type: 'keyword', + array: false, + required: false, + }, [Fields.ALERT_RULE_FROM]: { type: 'keyword', array: false, diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index d822d9316ad19..05a71677c7535 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -119,6 +119,7 @@ function createRule(shouldWriteAlerts: boolean = true) { tags: ['tags'], updatedBy: 'updatedBy', namespace: 'namespace', + executionId: 'b33f65d7-6e8b-4aae-8d20-c93613dec9f9', })) ?? {}) as Record; previousStartedAt = startedAt; @@ -224,6 +225,7 @@ describe('createLifecycleRuleTypeFactory', () => { "kibana.alert.instance.id": "opbeans-java", "kibana.alert.rule.category": "ruleTypeName", "kibana.alert.rule.consumer": "consumer", + "kibana.alert.rule.execution.uuid": "b33f65d7-6e8b-4aae-8d20-c93613dec9f9", "kibana.alert.rule.name": "name", "kibana.alert.rule.producer": "producer", "kibana.alert.rule.rule_type_id": "ruleTypeId", @@ -251,6 +253,7 @@ describe('createLifecycleRuleTypeFactory', () => { "kibana.alert.instance.id": "opbeans-node", "kibana.alert.rule.category": "ruleTypeName", "kibana.alert.rule.consumer": "consumer", + "kibana.alert.rule.execution.uuid": "b33f65d7-6e8b-4aae-8d20-c93613dec9f9", "kibana.alert.rule.name": "name", "kibana.alert.rule.producer": "producer", "kibana.alert.rule.rule_type_id": "ruleTypeId", diff --git a/x-pack/plugins/rule_registry/server/utils/get_common_alert_fields.ts b/x-pack/plugins/rule_registry/server/utils/get_common_alert_fields.ts index 8b4daf30763f6..db8c56f84b2c4 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_common_alert_fields.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_common_alert_fields.ts @@ -11,6 +11,7 @@ import { ALERT_UUID, ALERT_RULE_CATEGORY, ALERT_RULE_CONSUMER, + ALERT_RULE_EXECUTION_UUID, ALERT_RULE_NAME, ALERT_RULE_PRODUCER, ALERT_RULE_TYPE_ID, @@ -26,6 +27,7 @@ import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; const commonAlertFieldNames = [ ALERT_RULE_CATEGORY, ALERT_RULE_CONSUMER, + ALERT_RULE_EXECUTION_UUID, ALERT_RULE_NAME, ALERT_RULE_PRODUCER, ALERT_RULE_TYPE_ID, @@ -47,6 +49,7 @@ export const getCommonAlertFields = ( return { [ALERT_RULE_CATEGORY]: options.rule.ruleTypeName, [ALERT_RULE_CONSUMER]: options.rule.consumer, + [ALERT_RULE_EXECUTION_UUID]: options.executionId, [ALERT_RULE_NAME]: options.rule.name, [ALERT_RULE_PRODUCER]: options.rule.producer, [ALERT_RULE_TYPE_ID]: options.rule.ruleTypeId, diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts index 10cce043fe3fd..08b1b0a8ecbf2 100644 --- a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts +++ b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts @@ -79,4 +79,5 @@ export const createDefaultAlertExecutorOptions = < updatedBy: null, previousStartedAt: null, namespace: undefined, + executionId: 'b33f65d7-6e8b-4aae-8d20-c93613deb33f', }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 7b9dd63c73251..964819164b315 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -57,7 +57,7 @@ describe('Alert details with unmapped fields', () => { // This test needs to be updated to not look for the field in a specific row, as it prevents us from adding/removing fields it.skip('Displays the unmapped field on the table', () => { const expectedUnmmappedField = { - row: 82, + row: 83, field: 'unmapped', text: 'This is the unmapped field', }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts index f064380cc4a13..1d97b7a39779a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts @@ -39,6 +39,7 @@ describe('legacyRules_notification_alert_type', () => { payload = { alertId: '1111', + executionId: 'b33f65d7-b33f-4aae-8d20-c93613dec9f9', services: alertServices, params: { ruleAlertId: '2222' }, state: {}, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap index e03e438650df9..b826ed83d34ed 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap @@ -4164,6 +4164,13 @@ Object { "enabled": Object { "type": "keyword", }, + "execution": Object { + "properties": Object { + "uuid": Object { + "type": "keyword", + }, + }, + }, "false_positives": Object { "type": "keyword", }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json index 4f754ecd2d33a..6df246d06760d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json @@ -286,6 +286,13 @@ "enabled": { "type": "keyword" }, + "execution": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, "filters": { "type": "object" }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index 204c67bf6cf5a..263aa9e54737d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -170,6 +170,7 @@ export const previewRulesRoute = async ( statePreview = (await executor({ alertId: previewId, createdBy: rule.createdBy, + executionId: uuid.v4(), name: rule.name, params, previousStartedAt, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts index 22a41f356c226..7cf82722c47ff 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts @@ -23,6 +23,7 @@ const ruleExecutionLogClientMock = { const ruleExecutionLoggerMock = { create: (context: Partial = {}): jest.Mocked => ({ context: { + executionId: context.executionId ?? 'some execution id', ruleId: context.ruleId ?? 'some rule id', ruleName: context.ruleName ?? 'Some rule', ruleType: context.ruleType ?? 'some rule type', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/events_writer.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/events_writer.ts index dad30e9cb5d88..2869f1c2c82e8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/events_writer.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_events/events_writer.ts @@ -24,6 +24,7 @@ export interface IRuleExecutionEventsWriter { } export interface BaseArgs { + executionId: string; ruleId: string; ruleName: string; ruleType: string; @@ -49,7 +50,7 @@ export const createRuleExecutionEventsWriter = ( let sequence = 0; return { - logStatusChange({ ruleId, ruleName, ruleType, spaceId, newStatus, message }) { + logStatusChange({ executionId, ruleId, ruleName, ruleType, spaceId, newStatus, message }) { eventLogger.logEvent({ '@timestamp': nowISO(), message, @@ -69,6 +70,7 @@ export const createRuleExecutionEventsWriter = ( execution: { status: newStatus, status_order: ruleExecutionStatusOrderByStatus[newStatus], + uuid: executionId, }, }, }, @@ -85,7 +87,7 @@ export const createRuleExecutionEventsWriter = ( }); }, - logExecutionMetrics({ ruleId, ruleName, ruleType, spaceId, metrics }) { + logExecutionMetrics({ executionId, ruleId, ruleName, ruleType, spaceId, metrics }) { eventLogger.logEvent({ '@timestamp': nowISO(), rule: { @@ -103,6 +105,7 @@ export const createRuleExecutionEventsWriter = ( rule: { execution: { metrics, + uuid: executionId, }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger.ts index f67aae472ef60..a3e14f9569e25 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger.ts @@ -28,7 +28,7 @@ export const createRuleExecutionLogger = ( logger: Logger, context: RuleExecutionContext ): IRuleExecutionLogger => { - const { ruleId, ruleName, ruleType, spaceId } = context; + const { executionId, ruleId, ruleName, ruleType, spaceId } = context; const ruleExecutionLogger: IRuleExecutionLogger = { get context() { @@ -44,7 +44,7 @@ export const createRuleExecutionLogger = ( ]); } catch (e) { const logMessage = 'Error logging rule execution status change'; - const logAttributes = `status: "${args.newStatus}", rule id: "${ruleId}", rule name: "${ruleName}"`; + const logAttributes = `status: "${args.newStatus}", rule id: "${ruleId}", rule name: "${ruleName}", execution uuid: "${executionId}"`; const logReason = e instanceof Error ? e.stack ?? e.message : String(e); const logMeta: ExtMeta = { rule: { @@ -53,6 +53,7 @@ export const createRuleExecutionLogger = ( type: ruleType, execution: { status: args.newStatus, + uuid: executionId, }, }, kibana: { @@ -65,6 +66,7 @@ export const createRuleExecutionLogger = ( }, }; + // TODO: Add executionId to new status SO? const writeStatusChangeToSavedObjects = async ( args: NormalizedStatusChangeArgs ): Promise => { @@ -86,6 +88,7 @@ export const createRuleExecutionLogger = ( if (metrics) { eventsWriter.logExecutionMetrics({ + executionId, ruleId, ruleName, ruleType, @@ -95,6 +98,7 @@ export const createRuleExecutionLogger = ( } eventsWriter.logStatusChange({ + executionId, ruleId, ruleName, ruleType, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger_interface.ts index e31c10bd9747f..874d60cf4a401 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger_interface.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_logger/logger_interface.ts @@ -29,6 +29,7 @@ export interface IRuleExecutionLogger { } export interface RuleExecutionContext { + executionId: string; ruleId: string; ruleName: string; ruleType: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index a643f428e58e7..ba30b0335fb39 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -60,6 +60,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = return withSecuritySpan('scurityRuleTypeExecutor', async () => { const { alertId, + executionId, params, previousStartedAt, startedAt, @@ -81,6 +82,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = eventLogService, logger, { + executionId, ruleId: alertId, ruleName: rule.name, ruleType: rule.ruleTypeId, @@ -104,6 +106,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = const buildRuleMessage = buildRuleMessageFactory({ id: alertId, + executionId, ruleId, name, index: spaceId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/build_rule_message_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/build_rule_message_factory.ts index 6ebc902db6992..bac112bb3cab1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/build_rule_message_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/build_rule_message_factory.ts @@ -7,6 +7,7 @@ export type BuildRuleMessage = (...messages: string[]) => string; export interface BuildRuleMessageFactoryParams { + executionId: string; name: string; id: string; ruleId: string | null | undefined; @@ -15,12 +16,13 @@ export interface BuildRuleMessageFactoryParams { // TODO: change `index` param to `spaceId` export const buildRuleMessageFactory = - ({ id, ruleId, index, name }: BuildRuleMessageFactoryParams): BuildRuleMessage => + ({ executionId, id, ruleId, index, name }: BuildRuleMessageFactoryParams): BuildRuleMessage => (...messages) => [ ...messages, `name: "${name}"`, `id: "${id}"`, `rule id: "${ruleId ?? '(unknown rule id)'}"`, + `execution id: "${executionId}"`, `space ID: "${index}"`, ].join(' '); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts index 88b4ae01b3a64..07f2dfa31015c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts @@ -93,6 +93,8 @@ export const buildAncestors = (doc: SimpleHit): Ancestor[] => { * Builds the `kibana.alert.*` fields that are common across all alerts. * @param docs The parent alerts/events of the new alert to be built. * @param rule The rule that is generating the new alert. + * @param spaceId The space ID in which the rule was executed. + * @param reason Human readable string summarizing alert. */ export const buildAlert = ( docs: SimpleHit[], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index c620d51a83382..cae1019ae3f80 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -663,7 +663,6 @@ describe('utils', () => { }, }, }; - const ruleExecutionLogger = ruleExecutionLogMock.logger.create(); mockLogger.warn.mockClear(); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index 6b424adaa54ab..c0d05c44201fb 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -140,6 +140,7 @@ describe('alertType', () => { const result = await alertType.executor({ alertId: uuid.v4(), + executionId: uuid.v4(), startedAt: new Date(), previousStartedAt: new Date(), services: alertServices as unknown as AlertServices< @@ -218,6 +219,7 @@ describe('alertType', () => { const result = await alertType.executor({ alertId: uuid.v4(), + executionId: uuid.v4(), startedAt: new Date(), previousStartedAt: new Date(), services: alertServices as unknown as AlertServices< @@ -369,6 +371,7 @@ describe('alertType', () => { const result = await alertType.executor({ alertId: uuid.v4(), + executionId: uuid.v4(), startedAt: new Date(), previousStartedAt: new Date(), services: alertServices as unknown as AlertServices< @@ -446,6 +449,7 @@ describe('alertType', () => { const executorOptions = { alertId: uuid.v4(), + executionId: uuid.v4(), startedAt: new Date(), previousStartedAt: new Date(), services: alertServices as unknown as AlertServices< @@ -559,6 +563,7 @@ describe('alertType', () => { const result = await alertType.executor({ alertId: uuid.v4(), + executionId: uuid.v4(), startedAt: new Date(), previousStartedAt: new Date(), services: alertServices as unknown as AlertServices< @@ -642,6 +647,7 @@ describe('alertType', () => { const result = await alertType.executor({ alertId: uuid.v4(), + executionId: uuid.v4(), startedAt: new Date(), previousStartedAt: new Date(), services: alertServices as unknown as AlertServices< diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts index 7ab382ec77172..f6b1c4a3a3b0a 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts @@ -169,6 +169,7 @@ describe('alertType', () => { await alertType.executor({ alertId: uuid.v4(), + executionId: uuid.v4(), startedAt: new Date(), previousStartedAt: new Date(), services: alertServices as unknown as AlertServices<{}, ActionContext, typeof ActionGroupId>, @@ -230,6 +231,7 @@ describe('alertType', () => { await alertType.executor({ alertId: uuid.v4(), + executionId: uuid.v4(), startedAt: new Date(), previousStartedAt: new Date(), services: customAlertServices as unknown as AlertServices< @@ -295,6 +297,7 @@ describe('alertType', () => { await alertType.executor({ alertId: uuid.v4(), + executionId: uuid.v4(), startedAt: new Date(), previousStartedAt: new Date(), services: customAlertServices as unknown as AlertServices< diff --git a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts index a4cc9d5139148..3ee7929170338 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts @@ -16,7 +16,7 @@ interface EqualCondition { equal: number; } -function isEqualConsition( +function isEqualCondition( condition: GreaterThanEqualCondition | EqualCondition ): condition is EqualCondition { return Number.isInteger((condition as EqualCondition).equal); @@ -67,7 +67,7 @@ export async function getEventLog(params: GetEventLogParams): Promise= condition.gte) ) @@ -76,7 +76,7 @@ export async function getEventLog(params: GetEventLogParams): Promise event?.event?.action !== 'execute' ); + // Verify unique executionId generated per `action:execute` grouping + const eventExecutionIdSet = new Set(); + const totalUniqueExecutionIds = new Set(); + let totalExecutionEventCount = 0; + events.forEach((event) => { + totalUniqueExecutionIds.add(event?.kibana?.alert?.rule?.execution?.uuid); + if (event?.event?.action === 'execute') { + totalExecutionEventCount += 1; + eventExecutionIdSet.add(event?.kibana?.alert?.rule?.execution?.uuid); + expect(eventExecutionIdSet.size).to.equal(1); + eventExecutionIdSet.clear(); + } else { + eventExecutionIdSet.add(event?.kibana?.alert?.rule?.execution?.uuid); + } + }); + + // Ensure every execution actually had a unique id from the others + expect(totalUniqueExecutionIds.size).to.equal(totalExecutionEventCount); + const currentAlertSpan: { alertId?: string; start?: string; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts index cb1c41852465f..343db03c2ae27 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { ALERT_REASON, + ALERT_RULE_EXECUTION_UUID, ALERT_RULE_NAMESPACE, ALERT_RULE_PARAMETERS, ALERT_RULE_UPDATED_AT, @@ -120,6 +121,7 @@ export default ({ getService }: FtrProviderContext) => { expect(signal._source).eql({ '@timestamp': signal._source['@timestamp'], + [ALERT_RULE_EXECUTION_UUID]: signal._source[ALERT_RULE_EXECUTION_UUID], [ALERT_UUID]: signal._source[ALERT_UUID], [VERSION]: signal._source[VERSION], actual: [1], diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/create_rule.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/create_rule.ts index ac36bad1f595b..be3146f34c30e 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/create_rule.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/create_rule.ts @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import { ALERT_DURATION, ALERT_END, + ALERT_RULE_EXECUTION_UUID, ALERT_RULE_UUID, ALERT_START, ALERT_STATUS, @@ -187,7 +188,14 @@ export default function registryRulesApiTest({ getService }: FtrProviderContext) const alertEvent = afterViolatingDataResponse.hits.hits[0].fields as Record; - const exclude = ['@timestamp', ALERT_START, ALERT_UUID, ALERT_RULE_UUID, VERSION]; + const exclude = [ + '@timestamp', + ALERT_START, + ALERT_UUID, + ALERT_RULE_EXECUTION_UUID, + ALERT_RULE_UUID, + VERSION, + ]; const toCompare = omit(alertEvent, exclude); From 5f8e1867e97bc353e8c0ae1d4802de495ac2c0c4 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Fri, 21 Jan 2022 15:21:14 -0500 Subject: [PATCH 34/43] [Maps] fix typo (#123496) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/public/embeddable/map_embeddable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index a3b6638d5ee8b..af2d1577834b7 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -403,7 +403,7 @@ export class MapEmbeddable render( - {content}; + {content} , this._domNode From ac72ef20f8717db1aa92c6f0b5e805da8cb565c7 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Sun, 23 Jan 2022 08:43:35 -0500 Subject: [PATCH 35/43] skip failing test suite (#123456) --- test/functional/apps/console/_console.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 3f873850ff193..3d75c16c57c8f 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -27,7 +27,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'console']); const toasts = getService('toasts'); - describe('console app', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/123456 + describe.skip('console app', function describeIndexTests() { this.tags('includeFirefox'); before(async () => { log.debug('navigateTo console'); From 3db560b9386c9123f83e8d45a5c5d7441ed4c208 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 23 Jan 2022 11:31:50 -0500 Subject: [PATCH 36/43] Update dependency node-forge to ^1.2.1 (#123541) Co-authored-by: Renovate Bot --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 2dd2e411bdcfb..0ff755d675911 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "**/istanbul-lib-coverage": "^3.2.0", "**/json-schema": "^0.4.0", "**/minimist": "^1.2.5", - "**/node-forge": "^1.1.0", + "**/node-forge": "^1.2.1", "**/pdfkit/crypto-js": "4.0.0", "**/react-syntax-highlighter": "^15.3.1", "**/react-syntax-highlighter/**/highlight.js": "^10.4.1", @@ -302,7 +302,7 @@ "mustache": "^2.3.2", "nock": "12.0.3", "node-fetch": "^2.6.1", - "node-forge": "^1.1.0", + "node-forge": "^1.2.1", "nodemailer": "^6.6.2", "normalize-path": "^3.0.0", "object-hash": "^1.3.1", diff --git a/yarn.lock b/yarn.lock index f840945352c91..e3d2628f89f34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20434,10 +20434,10 @@ node-fetch@2.6.1, node-fetch@^1.0.1, node-fetch@^2.3.0, node-fetch@^2.6.1: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-forge@^0.10.0, node-forge@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.1.0.tgz#53e61b039eea78b442a4e13f9439dbd61b5cd3a8" - integrity sha512-HeZMFB41cirRysIhIFFgORmR51/qhkjRTXXIH9QiwS3AjF9L9Kre9XvOnyE7NMubOSHDuN0GsrFpnqhlJcNWTA== +node-forge@^0.10.0, node-forge@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c" + integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w== node-gyp-build@^4.2.3: version "4.2.3" From a2677b816337d5fe733dac74d9dba95573f554cf Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Sun, 23 Jan 2022 16:12:23 -0500 Subject: [PATCH 37/43] Revert "skip failing test suite (#123456)" This reverts commit ac72ef20f8717db1aa92c6f0b5e805da8cb565c7. --- test/functional/apps/console/_console.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 3d75c16c57c8f..3f873850ff193 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -27,8 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'console']); const toasts = getService('toasts'); - // Failing: See https://github.com/elastic/kibana/issues/123456 - describe.skip('console app', function describeIndexTests() { + describe('console app', function describeIndexTests() { this.tags('includeFirefox'); before(async () => { log.debug('navigateTo console'); From 556d00d3a8caf70800c4b790b5acef6f67e68bd1 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Sun, 23 Jan 2022 16:15:32 -0500 Subject: [PATCH 38/43] skip single flaky test #123556 --- test/functional/apps/console/_console.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 3f873850ff193..7a8a36bec56d7 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -92,7 +92,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); - it('should add comma after previous non empty line on autocomplete', async () => { + // Flaky, see https://github.com/elastic/kibana/issues/123556 + it.skip('should add comma after previous non empty line on autocomplete', async () => { const LINE_NUMBER = 2; await PageObjects.console.dismissTutorial(); From ed5185283f944797d54e4c9e9dd45197c0fdc244 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Mon, 24 Jan 2022 10:15:03 +0100 Subject: [PATCH 39/43] [SLM] Hide system indices in snapshot restore flow (#123365) * Dedupe system indices from indices array * commit using @elastic.co Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/lib/snapshot_serialization.test.ts | 16 ++++++++-- .../common/lib/snapshot_serialization.ts | 32 +++++++++++++++---- .../snapshot_restore/common/types/snapshot.ts | 4 +++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts index de769686dc99c..22e1e9329ca2c 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts @@ -18,7 +18,7 @@ describe('Snapshot serialization and deserialization', () => { repository: 'repositoryName', version_id: 5, version: 'version', - indices: ['index2', 'index3', 'index1'], + indices: ['index2', 'index3', 'index1', '.kibana'], include_global_state: false, state: 'SUCCESS', start_time: '0', @@ -31,6 +31,12 @@ describe('Snapshot serialization and deserialization', () => { failed: 1, successful: 2, }, + feature_states: [ + { + feature_name: 'kibana', + indices: ['.kibana'], + }, + ], failures: [ { index: 'z', @@ -71,6 +77,12 @@ describe('Snapshot serialization and deserialization', () => { failed: 1, successful: 2, }, + feature_states: [ + { + feature_name: 'kibana', + indices: ['.kibana'], + }, + ], failures: [ { index: 'z', @@ -98,7 +110,7 @@ describe('Snapshot serialization and deserialization', () => { uuid: 'UUID', versionId: 5, version: 'version', - // Indices are sorted. + // Indices are sorted and dont include any of the system indices listed in feature_state indices: ['index1', 'index2', 'index3'], dataStreams: [], includeGlobalState: false, diff --git a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts index f2803a571c475..e6e047cfa5d7d 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts @@ -6,6 +6,7 @@ */ import { sortBy } from 'lodash'; +import { flow, map, flatten, uniq } from 'lodash/fp'; import { SnapshotDetails, @@ -20,6 +21,19 @@ import { deserializeTime, serializeTime } from './time_serialization'; import { csvToArray } from './utils'; +export const convertFeaturesToIndicesArray = ( + features: SnapshotDetailsEs['feature_states'] +): string[] => { + return flow( + // Map each feature into Indices[] + map('indices'), + // Flatten the array + flatten, + // And finally dedupe the indices + uniq + )(features); +}; + export function deserializeSnapshotDetails( snapshotDetailsEs: SnapshotDetailsEs, managedRepository?: string, @@ -46,21 +60,27 @@ export function deserializeSnapshotDetails( duration_in_millis: durationInMillis, failures = [], shards, + feature_states: featureStates = [], metadata: { policy: policyName } = { policy: undefined }, } = snapshotDetailsEs; + const systemIndices = convertFeaturesToIndicesArray(featureStates); + const snapshotIndicesWithoutSystemIndices = indices + .filter((index) => !systemIndices.includes(index)) + .sort(); + // If an index has multiple failures, we'll want to see them grouped together. - const indexToFailuresMap = failures.reduce((map, failure) => { + const indexToFailuresMap = failures.reduce((aggregation, failure) => { const { index, ...rest } = failure; - if (!map[index]) { - map[index] = { + if (!aggregation[index]) { + aggregation[index] = { index, failures: [], }; } - map[index].failures.push(rest); - return map; + aggregation[index].failures.push(rest); + return aggregation; }, {}); // Sort all failures by their shard. @@ -80,7 +100,7 @@ export function deserializeSnapshotDetails( uuid, versionId, version, - indices: [...indices].sort(), + indices: snapshotIndicesWithoutSystemIndices, dataStreams: [...dataStreams].sort(), includeGlobalState, state, diff --git a/x-pack/plugins/snapshot_restore/common/types/snapshot.ts b/x-pack/plugins/snapshot_restore/common/types/snapshot.ts index 97f3b00d97326..baddcac7f5094 100644 --- a/x-pack/plugins/snapshot_restore/common/types/snapshot.ts +++ b/x-pack/plugins/snapshot_restore/common/types/snapshot.ts @@ -68,6 +68,10 @@ export interface SnapshotDetailsEs { duration_in_millis: number; failures: any[]; shards: SnapshotDetailsShardsStatusEs; + feature_states: Array<{ + feature_name: string; + indices: string[]; + }>; metadata?: { policy: string; [key: string]: any; From a6853ec46e15a9422edeb0c6a381dc409c081a42 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 24 Jan 2022 10:24:56 +0100 Subject: [PATCH 40/43] [ML] Remove export wildcard syntax for data visualizer plugin. (#123213) Remove export wildcard syntax for data visualizer plugin. --- x-pack/plugins/data_visualizer/common/index.ts | 12 ------------ x-pack/plugins/data_visualizer/kibana.json | 3 --- .../expanded_row/file_based_expanded_row.tsx | 2 +- .../geo_point_content_with_map.tsx | 2 +- .../expanded_row/index_based_expanded_row.tsx | 2 +- .../components/field_data_row/action_menu/actions.ts | 2 +- .../field_data_row/action_menu/lens_utils.ts | 2 +- .../field_type_icon/field_type_icon.test.tsx | 2 +- .../components/field_type_icon/field_type_icon.tsx | 2 +- .../components/fields_stats_grid/create_fields.ts | 2 +- .../fields_stats_grid/fields_stats_grid.tsx | 2 +- .../components/fields_stats_grid/filter_fields.ts | 2 +- .../components/fields_stats_grid/get_field_names.ts | 4 ++-- .../stats_table/data_visualizer_stats_table.tsx | 3 ++- .../components/stats_table/use_table_settings.ts | 2 +- .../application/common/components/utils/utils.ts | 2 +- .../common/util/field_types_utils.test.ts | 2 +- .../application/common/util/field_types_utils.ts | 2 +- .../components/analysis_summary/analysis_summary.tsx | 2 +- .../components/edit_flyout/options/options.ts | 2 +- .../components/edit_flyout/overrides.js | 2 +- .../components/edit_flyout/overrides.test.js | 2 +- .../file_error_callouts.tsx | 2 +- .../index_data_visualizer_view.tsx | 2 +- .../components/search_panel/field_type_filter.tsx | 2 +- .../embeddables/grid_embeddable/grid_embeddable.tsx | 2 +- .../hooks/use_data_visualizer_grid_data.ts | 5 ++--- .../index_data_visualizer/hooks/use_field_stats.ts | 2 +- .../search_strategy/requests/get_fields_stats.ts | 2 +- .../utils/saved_search_utils.test.ts | 2 +- .../plugins/data_visualizer/public/register_home.ts | 7 ++++++- .../server/register_custom_integration.ts | 2 +- 32 files changed, 38 insertions(+), 48 deletions(-) delete mode 100644 x-pack/plugins/data_visualizer/common/index.ts diff --git a/x-pack/plugins/data_visualizer/common/index.ts b/x-pack/plugins/data_visualizer/common/index.ts deleted file mode 100644 index 58159dfc3d7ef..0000000000000 --- a/x-pack/plugins/data_visualizer/common/index.ts +++ /dev/null @@ -1,12 +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. - */ - -// TODO: https://github.com/elastic/kibana/issues/110898 -/* eslint-disable @kbn/eslint/no_export_all */ - -export * from './constants'; -export * from './types'; diff --git a/x-pack/plugins/data_visualizer/kibana.json b/x-pack/plugins/data_visualizer/kibana.json index 9687a896c2c41..ea6f338ca07d0 100644 --- a/x-pack/plugins/data_visualizer/kibana.json +++ b/x-pack/plugins/data_visualizer/kibana.json @@ -30,9 +30,6 @@ "fieldFormats", "uiActions" ], - "extraPublicDirs": [ - "common" - ], "owner": { "name": "Machine Learning UI", "githubTeam": "ml-ui" diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx index 7ba1615e22b43..07e5d01e71d9b 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx @@ -16,7 +16,7 @@ import { NumberContent, } from '../stats_table/components/field_data_expanded_row'; import { GeoPointContent } from './geo_point_content/geo_point_content'; -import { JOB_FIELD_TYPES } from '../../../../../common'; +import { JOB_FIELD_TYPES } from '../../../../../common/constants'; import type { FileBasedFieldVisConfig } from '../../../../../common/types/field_vis_config'; export const FileBasedDataVisualizerExpandedRow = ({ item }: { item: FileBasedFieldVisConfig }) => { diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/geo_point_content_with_map/geo_point_content_with_map.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/geo_point_content_with_map/geo_point_content_with_map.tsx index 5da44262d29da..0c1c02ec033b2 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/geo_point_content_with_map/geo_point_content_with_map.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/geo_point_content_with_map/geo_point_content_with_map.tsx @@ -13,7 +13,7 @@ import { DocumentStatsTable } from '../../stats_table/components/field_data_expa import { ExamplesList } from '../../examples_list'; import { FieldVisConfig } from '../../stats_table/types'; import { useDataVisualizerKibana } from '../../../../kibana_context'; -import { JOB_FIELD_TYPES } from '../../../../../../common'; +import { JOB_FIELD_TYPES } from '../../../../../../common/constants'; import { ES_GEO_FIELD_TYPE, LayerDescriptor } from '../../../../../../../maps/common'; import { EmbeddedMapComponent } from '../../embedded_map'; import { ExpandedRowPanel } from '../../stats_table/components/field_data_expanded_row/expanded_row_panel'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx index b87da2b3da789..dd82fb4df33b8 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { GeoPointContentWithMap } from './geo_point_content_with_map'; -import { JOB_FIELD_TYPES } from '../../../../../common'; +import { JOB_FIELD_TYPES } from '../../../../../common/constants'; import { BooleanContent, DateContent, diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/actions.ts b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/actions.ts index 831cf8aab02a9..34023691307d0 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/actions.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/actions.ts @@ -17,7 +17,7 @@ import { dataVisualizerRefresh$, Refresh, } from '../../../../index_data_visualizer/services/timefilter_refresh_service'; -import { JOB_FIELD_TYPES } from '../../../../../../common'; +import { JOB_FIELD_TYPES } from '../../../../../../common/constants'; import { VISUALIZE_GEO_FIELD_TRIGGER } from '../../../../../../../../../src/plugins/ui_actions/public'; export function getActions( diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/lens_utils.ts b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/lens_utils.ts index 70aa103b86d53..3db1795456127 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/lens_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/lens_utils.ts @@ -18,7 +18,7 @@ import type { XYLayerConfig, } from '../../../../../../../lens/public'; import { FieldVisConfig } from '../../stats_table/types'; -import { JOB_FIELD_TYPES } from '../../../../../../common'; +import { JOB_FIELD_TYPES } from '../../../../../../common/constants'; interface ColumnsAndLayer { columns: Record; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx index 0c036dd6c6d76..874cdaa670c49 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { FieldTypeIcon } from './field_type_icon'; -import { JOB_FIELD_TYPES } from '../../../../../common'; +import { JOB_FIELD_TYPES } from '../../../../../common/constants'; describe('FieldTypeIcon', () => { test(`render component when type matches a field type`, () => { diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx index 8699523057510..ea24ba91cf7f2 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx @@ -10,7 +10,7 @@ import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FieldIcon } from '@kbn/react-field'; import { getJobTypeLabel } from '../../util/field_types_utils'; -import type { JobFieldType } from '../../../../../common'; +import type { JobFieldType } from '../../../../../common/types'; import './_index.scss'; interface FieldTypeIconProps { diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/create_fields.ts b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/create_fields.ts index f80ccd42919e2..26002f0404c44 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/create_fields.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/create_fields.ts @@ -8,7 +8,7 @@ import { FindFileStructureResponse } from '../../../../../../file_upload/common'; import { getFieldNames, getSupportedFieldType } from './get_field_names'; import { FileBasedFieldVisConfig } from '../stats_table/types'; -import { JOB_FIELD_TYPES } from '../../../../../common'; +import { JOB_FIELD_TYPES } from '../../../../../common/constants'; import { roundToDecimalPlace } from '../utils'; export function createFields(results: FindFileStructureResponse) { diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx index 1173ede84e631..dae5c6524d168 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx @@ -8,7 +8,7 @@ import React, { useMemo, FC, useState } from 'react'; import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import type { FindFileStructureResponse } from '../../../../../../file_upload/common'; -import type { DataVisualizerTableState } from '../../../../../common'; +import type { DataVisualizerTableState } from '../../../../../common/types'; import { DataVisualizerTable, ItemIdToExpandedRowMap } from '../stats_table'; import type { FileBasedFieldVisConfig } from '../../../../../common/types/field_vis_config'; import { FileBasedDataVisualizerExpandedRow } from '../expanded_row'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts index 9f1ea4af22537..de97b6007d877 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JOB_FIELD_TYPES } from '../../../../../common'; +import { JOB_FIELD_TYPES } from '../../../../../common/constants'; import type { FileBasedFieldVisConfig, FileBasedUnknownFieldVisConfig, diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/get_field_names.ts b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/get_field_names.ts index 6ca6421bbd124..8ac53aaf3b3a7 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/get_field_names.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/get_field_names.ts @@ -8,8 +8,8 @@ import { difference } from 'lodash'; import { ES_FIELD_TYPES } from '../../../../../../../../src/plugins/data/common'; import type { FindFileStructureResponse } from '../../../../../../file_upload/common'; -import type { JobFieldType } from '../../../../../common'; -import { JOB_FIELD_TYPES } from '../../../../../common'; +import type { JobFieldType } from '../../../../../common/types'; +import { JOB_FIELD_TYPES } from '../../../../../common/constants'; export function getFieldNames(results: FindFileStructureResponse) { const { mappings, field_stats: fieldStats, column_names: columnNames } = results; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx index 976afc464a672..34b40e73571ba 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx @@ -23,7 +23,8 @@ import { import { i18n } from '@kbn/i18n'; import { EuiTableComputedColumnType } from '@elastic/eui/src/components/basic_table/table_types'; import { throttle } from 'lodash'; -import { JOB_FIELD_TYPES, JobFieldType, DataVisualizerTableState } from '../../../../../common'; +import { JOB_FIELD_TYPES } from '../../../../../common/constants'; +import type { JobFieldType, DataVisualizerTableState } from '../../../../../common/types'; import { DocumentStat } from './components/field_data_row/document_stats'; import { IndexBasedNumberContentPreview } from './components/field_data_row/number_content_preview'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts index 87d936edc2957..778aaa3697c7b 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts @@ -8,7 +8,7 @@ import { Direction, EuiBasicTableProps, Pagination, PropertySort } from '@elastic/eui'; import { useCallback, useMemo } from 'react'; -import { DataVisualizerTableState } from '../../../../../common'; +import type { DataVisualizerTableState } from '../../../../../common/types'; const PAGE_SIZE_OPTIONS = [10, 25, 50, 100]; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/utils/utils.ts b/x-pack/plugins/data_visualizer/public/application/common/components/utils/utils.ts index 5f9317be6d7c5..fdac866c286c2 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/utils/utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/utils/utils.ts @@ -7,7 +7,7 @@ import { isEqual } from 'lodash'; import type { AnalysisResult, InputOverrides } from '../../../../../../file_upload/common'; -import { MB, FILE_FORMATS } from '../../../../../common'; +import { MB, FILE_FORMATS } from '../../../../../common/constants'; export const DEFAULT_LINES_TO_SAMPLE = 1000; const UPLOAD_SIZE_MB = 5; diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts index 710ba12313f17..8f9e4ffd3b898 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JOB_FIELD_TYPES } from '../../../../common'; +import { JOB_FIELD_TYPES } from '../../../../common/constants'; import { getJobTypeLabel, jobTypeLabels } from './field_types_utils'; describe('field type utils', () => { diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts index 1fda7140dbab2..33931ea74b4ea 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { JOB_FIELD_TYPES } from '../../../../common'; +import { JOB_FIELD_TYPES } from '../../../../common/constants'; import type { IndexPatternField } from '../../../../../../../src/plugins/data/common'; import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/analysis_summary/analysis_summary.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/analysis_summary/analysis_summary.tsx index b754bfd4691ec..b0bd51e1b9a39 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/analysis_summary/analysis_summary.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/analysis_summary/analysis_summary.tsx @@ -10,7 +10,7 @@ import React, { FC } from 'react'; import { EuiTitle, EuiSpacer, EuiDescriptionList } from '@elastic/eui'; import type { FindFileStructureResponse } from '../../../../../../file_upload/common'; -import { FILE_FORMATS } from '../../../../../common'; +import { FILE_FORMATS } from '../../../../../common/constants'; export const AnalysisSummary: FC<{ results: FindFileStructureResponse }> = ({ results }) => { const items = createDisplayItems(results); diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/options/options.ts b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/options/options.ts index e0c3e7f2d58cc..8ff3de07cc81a 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/options/options.ts +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/options/options.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FILE_FORMATS } from '../../../../../../common/'; +import { FILE_FORMATS } from '../../../../../../common/constants'; import { TIMESTAMP_OPTIONS, diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.js b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.js index d0213813c8f16..9fbed83136f8f 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.js +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.js @@ -23,7 +23,7 @@ import { EuiTextArea, } from '@elastic/eui'; -import { FILE_FORMATS } from '../../../../../common'; +import { FILE_FORMATS } from '../../../../../common/constants'; import { getFormatOptions, diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.test.js b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.test.js index 8e9b0d0806fc1..8a4695f823128 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.test.js +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/edit_flyout/overrides.test.js @@ -7,7 +7,7 @@ import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { FILE_FORMATS } from '../../../../../common'; +import { FILE_FORMATS } from '../../../../../common/constants'; import { Overrides } from './overrides'; diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_error_callouts.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_error_callouts.tsx index 9daac8604d661..c2c0910098a18 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_error_callouts.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_error_callouts.tsx @@ -11,7 +11,7 @@ import React, { FC } from 'react'; import { EuiCallOut, EuiSpacer, EuiButtonEmpty, EuiHorizontalRule } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import { FILE_SIZE_DISPLAY_FORMAT } from '../../../../../common'; +import { FILE_SIZE_DISPLAY_FORMAT } from '../../../../../common/constants'; import { FindFileStructureErrorResponse } from '../../../../../../file_upload/common'; interface FileTooLargeProps { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx index 1c988869526b1..bbee06ef85c34 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx @@ -39,7 +39,7 @@ import { JobFieldType, SavedSearchSavedObject } from '../../../../../common/type import { useDataVisualizerKibana } from '../../../kibana_context'; import { FieldCountPanel } from '../../../common/components/field_count_panel'; import { DocumentCountContent } from '../../../common/components/document_count_content'; -import { OMIT_FIELDS } from '../../../../../common'; +import { OMIT_FIELDS } from '../../../../../common/constants'; import { kbnTypeToJobType } from '../../../common/util/field_types_utils'; import { SearchPanel } from '../search_panel'; import { ActionsPanel } from '../actions_panel'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx index ee54683b08435..0bf61865f30c6 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx @@ -8,7 +8,7 @@ import React, { FC, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { JobFieldType } from '../../../../../common'; +import type { JobFieldType } from '../../../../../common/types'; import { FieldTypeIcon } from '../../../common/components/field_type_icon'; import { MultiSelectPicker, Option } from '../../../common/components/multi_select_picker'; import { jobTypeLabels } from '../../../common/util/field_types_utils'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx index 2ce6670dfafef..e1757690e8963 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx @@ -39,7 +39,7 @@ import { } from '../../../common/components/stats_table'; import { FieldVisConfig } from '../../../common/components/stats_table/types'; import { getDefaultDataVisualizerListState } from '../../components/index_data_visualizer_view/index_data_visualizer_view'; -import { DataVisualizerTableState, SavedSearchSavedObject } from '../../../../../common'; +import type { DataVisualizerTableState, SavedSearchSavedObject } from '../../../../../common/types'; import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state'; import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row'; import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts index 1b209c28fdf56..1895b22fe8288 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts @@ -24,12 +24,11 @@ import { } from '../../../../../../../src/plugins/data/common'; import { FieldVisConfig } from '../../common/components/stats_table/types'; import { - FieldRequestConfig, JOB_FIELD_TYPES, - JobFieldType, NON_AGGREGATABLE_FIELD_TYPES, OMIT_FIELDS, -} from '../../../../common'; +} from '../../../../common/constants'; +import type { FieldRequestConfig, JobFieldType } from '../../../../common/types'; import { kbnTypeToJobType } from '../../common/util/field_types_utils'; import { getActions } from '../../common/components/field_data_row/action_menu'; import { DataVisualizerGridInput } from '../embeddables/grid_embeddable/grid_embeddable'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts index 64654d56db05b..bd60fec47b598 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts @@ -18,7 +18,7 @@ import type { Field, } from '../../../../common/types/field_stats'; import { useDataVisualizerKibana } from '../../kibana_context'; -import type { FieldRequestConfig } from '../../../../common'; +import type { FieldRequestConfig } from '../../../../common/types'; import type { DataVisualizerIndexBasedAppState } from '../types/index_data_visualizer_state'; import { buildBaseFilterCriteria, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_fields_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_fields_stats.ts index aa19aa9fbb495..747d4007ac1ff 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_fields_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_fields_stats.ts @@ -11,7 +11,7 @@ import type { FieldStatsError } from '../../../../../common/types/field_stats'; import type { ISearchOptions } from '../../../../../../../../src/plugins/data/common'; import { ISearchStart } from '../../../../../../../../src/plugins/data/public'; import type { FieldStats } from '../../../../../common/types/field_stats'; -import { JOB_FIELD_TYPES } from '../../../../../common'; +import { JOB_FIELD_TYPES } from '../../../../../common/constants'; import { fetchDateFieldsStats } from './get_date_field_stats'; import { fetchBooleanFieldsStats } from './get_boolean_field_stats'; import { fetchFieldsExamples } from './get_field_examples'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts index 60be59ab2db6c..6b72e8c09d600 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts @@ -10,7 +10,7 @@ import { createMergedEsQuery, getEsQueryFromSavedSearch, } from './saved_search_utils'; -import type { SavedSearchSavedObject } from '../../../../common'; +import type { SavedSearchSavedObject } from '../../../../common/types'; import type { SavedSearch } from '../../../../../../../src/plugins/discover/public'; import type { Filter, FilterStateStore } from '@kbn/es-query'; import { stubbedSavedObjectIndexPattern } from '../../../../../../../src/plugins/data_views/common/data_view.stub'; diff --git a/x-pack/plugins/data_visualizer/public/register_home.ts b/x-pack/plugins/data_visualizer/public/register_home.ts index 9338c93000ec9..0619382bd994c 100644 --- a/x-pack/plugins/data_visualizer/public/register_home.ts +++ b/x-pack/plugins/data_visualizer/public/register_home.ts @@ -9,7 +9,12 @@ import { i18n } from '@kbn/i18n'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { FileDataVisualizerWrapper } from './lazy_load_bundle/component_wrapper'; -import { featureTitle, FILE_DATA_VIS_TAB_ID, applicationPath, featureId } from '../common'; +import { + featureTitle, + FILE_DATA_VIS_TAB_ID, + applicationPath, + featureId, +} from '../common/constants'; export function registerHomeAddData(home: HomePublicPluginSetup) { home.addData.registerAddDataTab({ diff --git a/x-pack/plugins/data_visualizer/server/register_custom_integration.ts b/x-pack/plugins/data_visualizer/server/register_custom_integration.ts index 67be78277189b..5ff58c5ed4d90 100644 --- a/x-pack/plugins/data_visualizer/server/register_custom_integration.ts +++ b/x-pack/plugins/data_visualizer/server/register_custom_integration.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { CustomIntegrationsPluginSetup } from '../../../../src/plugins/custom_integrations/server'; -import { applicationPath, featureId, featureTitle } from '../common'; +import { applicationPath, featureId, featureTitle } from '../common/constants'; export function registerWithCustomIntegrations(customIntegrations: CustomIntegrationsPluginSetup) { customIntegrations.registerCustomIntegration({ From fd340b9a7cd1c91716ab4fc5eefd611bd2bd4dc1 Mon Sep 17 00:00:00 2001 From: Claudio Procida Date: Mon, 24 Jan 2022 10:50:22 +0100 Subject: [PATCH 41/43] chore: Adds test coverage and fixes for negative value formatters (#122272) * Adds tests for negative value formatters * Adds tests for negative duration formatters --- .../common/utils/formatters/duration.test.ts | 29 ++++++++++++++++++- .../utils/formatters/formatters.test.ts | 24 +++++++++++++++ .../common/utils/formatters/formatters.ts | 2 +- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability/common/utils/formatters/duration.test.ts b/x-pack/plugins/observability/common/utils/formatters/duration.test.ts index eaf073cfbb5b3..69f4792325a35 100644 --- a/x-pack/plugins/observability/common/utils/formatters/duration.test.ts +++ b/x-pack/plugins/observability/common/utils/formatters/duration.test.ts @@ -71,7 +71,7 @@ describe('duration formatters', () => { [1000, '1,000.0 tpm'], [1000000, '1,000,000.0 tpm'], ])( - 'displays the correct label when the number is integer and has zero decimals', + 'displays the correct label when the number is a positive integer and has zero decimals', (value, formattedValue) => { expect(asTransactionRate(value)).toBe(formattedValue); } @@ -89,12 +89,39 @@ describe('duration formatters', () => { expect(asTransactionRate(value)).toBe(formattedValue); } ); + + it.each([ + [-1, '< 0.1 tpm'], + [-10, '< 0.1 tpm'], + [-100, '< 0.1 tpm'], + [-1000, '< 0.1 tpm'], + [-1000000, '< 0.1 tpm'], + ])( + 'displays the correct label when the number is a negative integer and has zero decimals', + (value, formattedValue) => { + expect(asTransactionRate(value)).toBe(formattedValue); + } + ); + + it.each([ + [-1.23, '< 0.1 tpm'], + [-12.34, '< 0.1 tpm'], + [-123.45, '< 0.1 tpm'], + [-1234.56, '< 0.1 tpm'], + [-1234567.89, '< 0.1 tpm'], + ])( + 'displays the correct label when the number is negative and has decimal part', + (value, formattedValue) => { + expect(asTransactionRate(value)).toBe(formattedValue); + } + ); }); describe('asMilliseconds', () => { it('converts to formatted decimal milliseconds', () => { expect(asMillisecondDuration(0)).toEqual('0 ms'); }); + it('formats correctly with undefined values', () => { expect(asMillisecondDuration(undefined)).toEqual('N/A'); }); diff --git a/x-pack/plugins/observability/common/utils/formatters/formatters.test.ts b/x-pack/plugins/observability/common/utils/formatters/formatters.test.ts index c3b65ce7699d0..397eaae04b51a 100644 --- a/x-pack/plugins/observability/common/utils/formatters/formatters.test.ts +++ b/x-pack/plugins/observability/common/utils/formatters/formatters.test.ts @@ -108,5 +108,29 @@ describe('formatters', () => { ])('formats as decimal when number is below 10 ', (value, formattedValue) => { expect(asDecimalOrInteger(value)).toBe(formattedValue); }); + + it.each([ + [-0.123, '-0.1'], + [-1.234, '-1.2'], + [-9.876, '-9.9'], + ])( + 'formats as decimal when number is negative and below 10 in absolute value', + (value, formattedValue) => { + expect(asDecimalOrInteger(value)).toEqual(formattedValue); + } + ); + + it.each([ + [-12.34, '-12'], + [-123.45, '-123'], + [-1234.56, '-1,235'], + [-12345.67, '-12,346'], + [-12345678.9, '-12,345,679'], + ])( + 'formats as integer when number is negative and above or equals 10 in absolute value', + (value, formattedValue) => { + expect(asDecimalOrInteger(value)).toEqual(formattedValue); + } + ); }); }); diff --git a/x-pack/plugins/observability/common/utils/formatters/formatters.ts b/x-pack/plugins/observability/common/utils/formatters/formatters.ts index 9bdccc7e9edfe..05d8d2638ba7b 100644 --- a/x-pack/plugins/observability/common/utils/formatters/formatters.ts +++ b/x-pack/plugins/observability/common/utils/formatters/formatters.ts @@ -51,7 +51,7 @@ export type AsPercent = typeof asPercent; export function asDecimalOrInteger(value: number) { // exact 0 or above 10 should not have decimal - if (value === 0 || value >= 10) { + if (value === 0 || Math.abs(value) >= 10) { return asInteger(value); } return asDecimal(value); From 3eded8e680b8f2c3b7e9c5c12fb7fd238ce6926f Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 24 Jan 2022 11:08:16 +0100 Subject: [PATCH 42/43] [TSVB] Handle ignore daylight time correctly and fix shift problem (#123398) --- .../components/vis_types/timeseries/vis.js | 1 + .../vis_types/timeseries/vis.test.js | 60 ++++++++++++++++++- .../visualizations/views/timeseries/index.js | 5 +- .../response_processors/series/time_shift.js | 30 +++++++++- .../series/time_shift.test.js | 55 +++++++++++++++++ .../vis_data/series/handle_response_body.ts | 3 +- 6 files changed, 146 insertions(+), 8 deletions(-) diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js index b177ef632e210..2790130c553b5 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js @@ -266,6 +266,7 @@ class TimeseriesVisualization extends Component { legend={Boolean(model.show_legend)} legendPosition={model.legend_position} truncateLegend={Boolean(model.truncate_legend)} + ignoreDaylightTime={Boolean(model.ignore_daylight_time)} maxLegendLines={model.max_lines_legend} tooltipMode={model.tooltip_mode} xAxisFormatter={this.xAxisFormatter(interval)} diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.test.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.test.js index cf4c327df3d77..12ae70cca1036 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.test.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.test.js @@ -10,11 +10,12 @@ import React from 'react'; import { shallow } from 'enzyme'; import { TimeSeries } from '../../../visualizations/views/timeseries'; import TimeseriesVisualization from './vis'; -import { setFieldFormats } from '../../../../services'; +import { setFieldFormats, setCharts, setUISettings } from '../../../../services'; import { createFieldFormatter } from '../../lib/create_field_formatter'; import { FORMATS_UI_SETTINGS } from '../../../../../../../field_formats/common'; import { METRIC_TYPES } from '../../../../../../../data/common'; import { getFieldFormatsRegistry } from '../../../../../../../data/public/test_utils'; +import { MULTILAYER_TIME_AXIS_STYLE } from '../../../../../../../charts/public'; jest.mock('../../../../../../../data/public/services', () => ({ getUiSettings: () => ({ get: jest.fn() }), @@ -35,12 +36,47 @@ describe('TimeseriesVisualization', () => { }) ); - const setupTimeSeriesProps = (formatters, valueTemplates) => { + setCharts({ + theme: { + useChartsTheme: () => ({ + axes: { + tickLabel: { + padding: { + inner: 0, + }, + }, + }, + }), + useChartsBaseTheme: () => ({ + axes: { + tickLabel: { + padding: { + inner: 0, + }, + }, + }, + }), + }, + activeCursor: {}, + }); + + setUISettings({ + get: () => ({}), + isDefault: () => true, + }); + + const renderShallow = (formatters, valueTemplates, modelOverwrites) => { const series = formatters.map((formatter, index) => ({ id: id + index, + label: '', formatter, value_template: valueTemplates?.[index], data: [], + lines: { + show: true, + }, + points: {}, + color: '#000000', metrics: [ { type: METRIC_TYPES.AVG, @@ -63,6 +99,7 @@ describe('TimeseriesVisualization', () => { id, series, use_kibana_indexes: true, + ...modelOverwrites, }} visData={{ [id]: { @@ -75,9 +112,26 @@ describe('TimeseriesVisualization', () => { /> ); - return timeSeriesVisualization.find(TimeSeries).props(); + return timeSeriesVisualization; + }; + + const setupTimeSeriesProps = (formatters, valueTemplates) => { + return renderShallow(formatters, valueTemplates).find(TimeSeries).props(); }; + test('should enable new time axis if ignore daylight time setting is switched off', () => { + const component = renderShallow(['byte'], undefined, { ignore_daylight_time: false }); + console.log(component.find('TimeSeries').dive().debug()); + const xAxis = component.find('TimeSeries').dive().find('[id="bottom"]'); + expect(xAxis.prop('style')).toEqual(MULTILAYER_TIME_AXIS_STYLE); + }); + + test('should disable new time axis for ignore daylight time setting', () => { + const component = renderShallow(['byte'], undefined, { ignore_daylight_time: true }); + const xAxis = component.find('TimeSeries').dive().find('[id="bottom"]'); + expect(xAxis.prop('style')).toBeUndefined(); + }); + test('should return byte formatted value from yAxis formatter for single byte series', () => { const timeSeriesProps = setupTimeSeriesProps(['byte']); diff --git a/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js index 9dfddd3457d44..c8e845ce6b54c 100644 --- a/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js @@ -77,6 +77,7 @@ export const TimeSeries = ({ interval, isLastBucketDropped, useLegacyTimeAxis, + ignoreDaylightTime, }) => { // If the color isn't configured by the user, use the color mapping service // to assign a color from the Kibana palette. Colors will be shared across the @@ -152,7 +153,9 @@ export const TimeSeries = ({ const shouldUseNewTimeAxis = series.every( ({ stack, bars, lines }) => (bars?.show && stack !== STACKED_OPTIONS.NONE) || lines?.show - ) && !useLegacyTimeAxis; + ) && + !useLegacyTimeAxis && + !ignoreDaylightTime; return ( diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js index 109e552ce89a1..429050fab36cc 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js @@ -7,9 +7,18 @@ */ import { startsWith } from 'lodash'; -import moment from 'moment'; +import moment from 'moment-timezone'; -export function timeShift(resp, panel, series) { +export function timeShift( + resp, + panel, + series, + meta, + extractFields, + fieldFormatService, + cachedIndexPatternFetcher, + timezone +) { return (next) => (results) => { if (/^([+-]?[\d]+)([shmdwMy]|ms)$/.test(series.offset_time)) { const matches = series.offset_time.match(/^([+-]?[\d]+)([shmdwMy]|ms)$/); @@ -18,14 +27,29 @@ export function timeShift(resp, panel, series) { const offsetValue = matches[1]; const offsetUnit = matches[2]; + let defaultTimezone; + if (!panel.ignore_daylight_time) { + // the datemath plugin always parses dates by using the current default moment time zone. + // to use the configured time zone, we are switching just for the bounds calculation. + defaultTimezone = moment().zoneName(); + moment.tz.setDefault(timezone); + } + results.forEach((item) => { if (startsWith(item.id, series.id)) { item.data = item.data.map((row) => [ - moment.utc(row[0]).add(offsetValue, offsetUnit).valueOf(), + (panel.ignore_daylight_time ? moment.utc : moment)(row[0]) + .add(offsetValue, offsetUnit) + .valueOf(), row[1], ]); } }); + + if (!panel.ignore_daylight_time) { + // reset default moment timezone + moment.tz.setDefault(defaultTimezone); + } } } diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js index e00f4aad24130..7fff2603cf47a 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js @@ -73,4 +73,59 @@ describe('timeShift(resp, panel, series)', () => { [1483225210000 + 3600000, 2], ]); }); + + test('shifts in right timezone', async () => { + series.offset_time = '1d'; + const dateBeforeDST = new Date('2022-03-26T12:00:00.000Z').valueOf(); + const dateAfterDST = new Date('2022-03-28T12:00:00.000Z').valueOf(); + resp.aggregations.test.timeseries.buckets[0].key = dateBeforeDST; + resp.aggregations.test.timeseries.buckets[1].key = dateAfterDST; + const next = await timeShift( + resp, + panel, + series, + {}, + undefined, + undefined, + undefined, + 'Europe/Berlin' + )((results) => results); + const results = await stdMetric(resp, panel, series, {})(next)([]); + + expect(results).toHaveLength(1); + expect(results[0].data).toEqual([ + // only 23h in a day because it goes over the DST switch + [dateBeforeDST + 1000 * 60 * 60 * 23, 1], + // regular 24h in a day + [dateAfterDST + 1000 * 60 * 60 * 24, 2], + ]); + }); + + test('shifts in utc if ignore daylight time is set', async () => { + series.offset_time = '1d'; + panel.ignore_daylight_time = 1; + const dateBeforeDST = new Date('2022-03-26T12:00:00.000Z').valueOf(); + const dateAfterDST = new Date('2022-03-28T12:00:00.000Z').valueOf(); + resp.aggregations.test.timeseries.buckets[0].key = dateBeforeDST; + resp.aggregations.test.timeseries.buckets[1].key = dateAfterDST; + const next = await timeShift( + resp, + panel, + series, + {}, + undefined, + undefined, + undefined, + 'Europe/Berlin' + )((results) => results); + const results = await stdMetric(resp, panel, series, {})(next)([]); + + expect(results).toHaveLength(1); + expect(results[0].data).toEqual([ + // still 24h shift because DST is ignored + [dateBeforeDST + 1000 * 60 * 60 * 24, 1], + // regular 24h in a day + [dateAfterDST + 1000 * 60 * 60 * 24, 2], + ]); + }); }); diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/series/handle_response_body.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/series/handle_response_body.ts index 415844abeedaf..5244bca66a5b3 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/series/handle_response_body.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/series/handle_response_body.ts @@ -59,7 +59,8 @@ export function handleResponseBody( meta, extractFields, fieldFormatService, - services.cachedIndexPatternFetcher + services.cachedIndexPatternFetcher, + req.body.timerange.timezone ); return await processor([]); From 9cba74d03bc543821b6a63b9e52e3d05c2fa9876 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 24 Jan 2022 11:08:36 +0100 Subject: [PATCH 43/43] [Lens] Fix time range issue on save (#123536) --- x-pack/plugins/lens/public/app_plugin/mounter.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 73a25f36e22bd..ff45c996cce98 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -186,9 +186,8 @@ export async function mountApp( embeddableEditorIncomingState, initialContext, }; - const emptyState = getPreloadedState(storeDeps) as LensAppState; const lensStore: LensRootStore = makeConfigureStore(storeDeps, { - lens: emptyState, + lens: getPreloadedState(storeDeps) as LensAppState, } as PreloadedState); const EditorRenderer = React.memo( @@ -208,7 +207,7 @@ export async function mountApp( if (!initialContext) { data.query.filterManager.setAppFilters([]); } - lensStore.dispatch(setState(emptyState)); + lensStore.dispatch(setState(getPreloadedState(storeDeps) as LensAppState)); lensStore.dispatch(loadInitial({ redirectCallback, initialInput, history: props.history })); return (

      8w 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�i3L{~q@S(w8Srn4xQ45XM{JBl(&>OSf_5T!HE> zLSaD5H91$5k=5T6!&%2DBmksshS)0@wvz_Tt?ROD-8J>Zq9KLr*^whTDsGPJkRnYR zJ2CPW;Xko$;BKIr!I?eN8)mef^ErU+k1F5W+BsPE@``^SdjtM*Z^9m!K5G;KJRW(^ z6HjUJY4q#jX`(_$i#MEaTs~Tzaa80%1C03?n2r_9Aa2Z&ouYQ&0*&x%No+yWnCZhb z=G!ZI|L3x=F#c(N{pGgT@y-1rE|{?;f8F~ca7e&H3f~*f+U<&HwCJ;ci1Q-x@`-WE zP%SqPAX;_~33>LrV8}PR#R<*d@>k;vjI$}yZZ<(jlsn&togFdTA*n3rAvIi|D`nn6 z`~r1@GxajI^ZhOVD5cHpdKm{3q>%n-KDnDJR(k5E))&GSPQt9@s*%#!ISs9i<3;e~ zs@fF^IA#q2ayUaTc>|-^3d-8GK01l)_Bo(f0G1f^AnO@&E(@VXbbBl2TFxA_LU=NjWyU0dSr z^yzBXl~B32WVbmD%!9*|c2e4up;@EmyK0?3a{jwLTQc8QL=Y)_!g0&Z|RZ$d^Zvapvkr95P` zBMW_BkTV*-88xnN!$hLXixS@A`{!C2p08ZiB3||L6-5&^7dEd_lYrSg zNOf^k;}ZP-LyZ=1!#SV5KrgJ;%u&K!rD@F-nk#bmBn?F!Dszxc8sM40;^X=hg!n#0 zzRX80X(1WSnV@KWiF<3XDav9|cB^0;!WzOF{G5SoWSP(W@MC^y{R8b=2b(A)dc6z#6PjhppIusSnGWDP*=18aZJIHXAW zUbaTHs|({(9&^zp5U{%)#UAxB(m=rkWJ6anra{oD5JgtaIcOJS8RYsgF5#`0oCLu(pw|7|PH;@2SLz5<2YI2TOkY8@#*@e1( zxGphH@W;lIAn>q2n^_Fv?<3;10#N~ZY8HmgT5iJ@S0Scs4ZEDQPs9JB-3ZE>^5`H# zJwF)Ra1#Nfs62i=hS2t89``F9z9y4{<9b+>T}}2by^B3^*(0{>eJ5*zDoIWDPY$^F z^uXBm@!P=R05O8KPM2GT=P+6=rbK>9&$pIJcS!p`wx+ag__aN68*fcrj~PAgg{e8B z9NEO{3VggTY_P@ZD8<>oCTm@z#77}lb;E_3ZB?Tg27%^X_T9N7I93!^xt!|}R;ER- z3KBn|PM^DTcM{gI={!c7FZa=~ec=t`KuI2BFFoh=p7QliqERX3ud>Lg<1D)`^F6(< zXaoXDxh3G!Rjy~S{2UF}de?qI6jXs6x0w%R`duvDicX7Q%fsyv-k(5Qw!pmC$bSQE zLy%2btm%aTatAl#1VO(-xH6m@9%s4`%$3h`egV!vV)d+ z1ZFnjfRzYS3il5r#Na|GWbYV|*G!jJp677lIuA#{`w9gel3q|n$t1aMK6lk$5p2K{ z09lLib>^qM;{I5=FQvmZWY%-_{p-!S&izpz+S8?_JK9D?Yue4bMU>}ytF!(I>4imi zOD)rz#UD}{#ae?-0FDmud+hAxXa)RSta%D456lp~AzIeYu%h6pq5uj9SR5)8L($z= z(*xaYx6F`(5QERm)gaj8m%zdFr1aDV%?d{<3l^QHP18$eFV#@EN`%WHJrHsj6?`1T z3T~?M^w)W07;fx*)0V2eF)gm1Ypf;}{#pW*DcfN}Pe6-j^X_p69J;Le1%|TIoqoPJ zl5tL|zZgbpreQmQvz;43j-8udxI+O$Oq{k-&#F*dwU1Ym-`eSGuZdAK;@%1wl==v& zmzr;DK4>eaJV73OsTncdw((?irwlsO@Hcs0A=cJ^CF}fkNjgu8#sxX>Ah$B@q%%Oj zOG}aN4^%Q?GjhJK_CU%sMedaP`ZP8&g%v1j?lP^SM9U$d5x!~!TlX^sLRX+aW$xFy ze1P>&NP9>SdHcU1-n4yGOj#bS9$Rp;0myg}>;%84NP-YGF>6v8K~)%Pje@s|V$_xt z3K-#-uj|k@Cy~2sp&TP?p#H*}t#+J`tK0Wmv8VU{gzPr%6Z)^T+uhsuYD z%{)4S!GD){o1YuOTMMiHbrin;S9Ca{qkxk3He}?Guo*pc^%LI8we(8VgjwVEY80L_N^amj-m`LE%8>Tp>Z;f*rL2X#=4MM<2E^QA%>8Oo zogyej^AMVA4ej=s;EuH+x+6b>JbJ)Gzapeu~1JZxQ?{b6jyVd|2gNuGc>Ys8R za{ih#(iN?v!iVfUH3qvEsQeReKa$T7q5puUTJUvot{Vle+^WbRd;Wi<-GF~^x3)Ll zi{89HRypy1Lu+wWV8V{1u@4TY4gRQ9fb2SwH z>Y`E0JH|)U7_f%y1^%S)4)zV-%BLDU7L3yhx~BvJq>@Xu}gD zM+Mhrlv!wYl|?00M32|vmM(R$l;c{BPwcZIp;d2b? z(j2hyL{VTzWGH@y9FhZ<-lkQ6%bgIA{SY-g>b{YNY0^ic`m5OuJ~inL^ZQKA5mHe$ zu2`>#qz>Ra;^QtpZb*6^UfQTdaZ-m=zbC@`xNS*wLv-||{(kymM6S6j%eVCs1y97Yf-wYUz0Vo z7rB&;|4cx<{RZKYE|>5S77EW2PSoz-+5m&UMFtG6^xyNJ@8DpqH|0S7Td>=|$+|^7 z-nc4l{W!Tk@g!5-Lhd@Swvw&g^ zepjWyDONN@&lyXOY^M?-d?KVy9l$2@f=p;G+Mai;F4b#@w=wl!3BbRW#`kcamUVdV zTp^e=2qLxhefgBiQwE4heBn%x69Lt!eC#| z@!DXY^m}(9D46mJRHz)CyI$CLhMrgm7=uN^h_)_*T@+C}2j&0@Iu{;-Y`lzHnz^u6 zR1#DngfSKZR^2Cj;$Y*ZPau5?VSLf9sY&H>YG3FXNOmOIG+8$=o$?Aq_;vp1D4j%; z3P2qt(J`o_j4!~jwNpMUa~;uNAWr>Wha80}U+q~TCvR1R**44RPku#Xg!?v3tCDMQ=&9g6*6nMe3h2 zOkI|YDO!jdl}8M2VvR<@O>E7M?EQ(4{q-m3_MQ{q|B8Oo=lT`H>__${v3^TP*hq<3 z7<>T6gbMLxhrmghxQw%;@=G#lyaUadyW2LO_X_XbXC^Lpg%sVRM3D zxmxapte=oS?cEWCp_s9PQ%MzWbhg$`2R&1D`_5l*=1U2a0fo7X3Stz7d^>>YSCOQ@ z>l>~6`?(oGJaRMp?}MLXmm{+WHA^FiTlKh%DOq6@Z!pkU*#AG~M?#p&PTMu*il~k4kiS zIw~K{{-m>+)qUS5(A8r0;Ye-$AxM(P*Y>LG{?ymx?s!gVA4~M)xT4E>>vHjA-E6Hu zOB$22BDCp$$-(_UF}MScV;2BxXwO+2KW)${8@;K##rmUO+Ihoarhkdz?ZMejsSw|h zM0*Pkh6S5SiC723<7<7coGJURdWlmtzu$^%xP+=LQd##>5{eh5CHb9x&S+-njZ!;N z)YtjVZ(!tD$CAw6KN6fSWzUT@Ww43HTjT0iA9k*?R?Z2i%Fe*JUb6v3>lM)ACO1wQ zz6Z`(a$Rdv=5u_$rMV?k8J^VBieHSdIerV^90V%f%mhi*RxV3*iRw1qYk#0Lncrhx z?nIr7SZgK>sckB!8W0jQ`EZ?tGiyhx!zb6q$DRIYG2L0bG>&}vItESAKGZR{iMn)c z^qjQNohG5<GwAk*^9M;S&jyx^s+S@gd%RuAPHw=Qf9YAO#7wvWO=DN@$$aY4=M9_ zY2e*)PlJ}=_nw)=2}Bd>@3jsaua5Rl_>V6^JCk7lH#3I1#w&MVN-@C8%lP{Ea}1?W z_Elz+MAk8p{LFT^gXsr_M_S)6QKJKHpT{IcS>Qi#_$7J~e8n;I!SeJ#{LS`w-0`p- z>(3jvsUXRo|FQ)aoT)FhNPCdqO z(^8jKpSv?;$%a(NUu2Cf>31zQFzSTyB5u^* z9p-s}!x$T5!DZs4guGCb{9B3E7&?KNV81>4{ zJ8DIiYeJrk^sI=u2CgX~zaiVE0bI8EytcLCEYM+(<_nsZF6v; zYVp(P+y}h?%bbnENuNY8T-Y1p4UTmV?j15KQ4XsX-Kad=5%nqL+3RH2UZZTaBNs1C zZ~KT1{;xigLct6fj{ywVHd{tE=Jxi^rlVUQXz!C1HhAk+_R!9Wdl);dFY|7Oj z+~cMp!Ym9s43d;{20q)38r!-%tI63FJ0C=+!Asa$yT|tRcRzDd$#@(zyJ?SrabFZL zeb?euML4_hoHrJ%ntA8AQ2_+_wTr6U_`wB^d1V&;+#>{i5Ireoh=NQ5u&f)|N&izz!oCD?#pJiBL^7(5rp9LlmeD+2iMD8v} zISWK06bJK;*OyLs7nmWcJ%JP?Evx0^%(lF$@IObLWLS~}3_Mwly)aPH>~@>zdCtU5 zhqNC?{)D=40><-_v(SrffTKu#`a4RXpKYfTp4kKV;sWyq^M0R(Yp4ldQF*yniJv2% z%;<68EYiGnnLCG-bK_)bXs)_t)ZWUO*Jg9wK1t*46 z6Hd6=n|F+9G}B8+izU%PNNYrQNdnjR(+&-S7NR%;DRuLwn{juke`WIVARL&`TZKCp zw!@^y$7bTMw|kE&fg)_{0^y%lO+MHi>MKVy82H&3`GpDc`@Z50RyeXmqrQiFW8`t; z$Vaw!06bgR2-3Pi2+2y#q(|dI@$gqo^Z?l6ckM4q-#I~)Y9@bsw zuqgBH#BvhXCg0dP|5Zx($Dg>!g12K$L`=A>xb&)i!gKVvj_JRrjj1*&V~smrH*s0L zh1+i|b^GzNYS*X%qGhbiCC;0x)l?#k+4KZ3$V?008&uQ|{Lxz38vQN<2-+*3+c%k4kwZCwJ@Fe^84$h;KdW0NnBeB zR+J2ez3v(mEI4e7&SRrqh~7|mVOewgSNN}K8&|>nZ=)XY2li7fEu-vXI6fR1!09Lc=&?TeIsP1k2#r8Q3Nh~A5yFb_x#4+02;)9z@{%z$Is)qE zg_GyOIxUTZkErXDpMpQU+5kR`dznI_iiSWdSvlbrhoTX)nvfFKhD=&*AX~N`3YCcm zjtG(U2d3NGO#ZUTr?AaGPSv}T|Wl39m;#&cv>5 zA6ZC4xBv{s1FIS7xs9h@TgEZ`KO{Q>Q<1Ibd#ZSwp(5be9^WoR48$nE5c>;FuT zgibJBn&9rwB>RFqbqS6U_Oapb7;)ZvV7l~N1UOmFq0R!c_=S2RMI9c8>h)h+EjOYA z8?ZfBu>N=<8(ypiegxGa5G)8)EKu#_!n5GX-5@>2Ihskk8Sy+sEaLt-g+*d#ARbKV z-xRBJ9+ckKkw6_qxv)Qrt14^Vq3fg<}k%de9uoJxASi6q+!;Ii)b~!Rt4po$3$(2Tv*-P)7I&es|?pG+h5-s53?D8M}CVN z(~{M}(K8pz2`XJ9A>9(cbU|*uAYQ?2cD`QIBZ9W8a*aoYw_#tojV4`{6ceUCe*Bz) zSbywe|FYrqlj480P54!-+c)x0{3XVdxMLc8SdDY4a=-Im{cKMW%ncbgdg2`Bncy+y z`)XIc;Yg@$D!_OML@V2EdJLVvx2PA=8eu8=eCBc!agY#@2D(%Q7t~-4IR@TKzlRedW%|>NdIKw`kWZo&zvNe2v0ea zMq+;vhM9mGYuQoc%)ic?{pKhiAjgd6p(WPt-43EqmJuHm{ls2~fF%Q8JqD5Y+P8&{ zphORyNxfxM=A&HPLqKl$V4*%la?}9+Mfh?aSjdpfD`>TQM6#a zb40#cVpC#Bwxh|*bv7(FcH9BpULjNY?f_W6%cH!k3rww8W_Vr0x0Q#E%Hd~zK_AHe;M z?!RXeKo{&Hqd7GCg%y)vm}{CEp8)#D@0}2<0*~Dk{6JXQW0#)Od!9n2Zem#`M~O1v zm3BQ7)PUC93!_(_eTCXb&N`GI1HUs32+<`UTJv}Asq`Kz!_X8FV)Ss@{gAm>hd(U# z{`QerS|ZJ~kx3XgK0zum!Z7PH+ltTpE4)~S`4BYJ(=C? zQ&w#j4P&P!mPx4#ZB|NRodt4~IVR{l3jvIhTdl!q3bK%%5lGPT%u3%c)Jj2qT+LF% z^O+w{80Ce0&rJMYRBKPI>?lm(y8+%#^SpMJm5E&BFjYictFy%m#bDQurxDWt`#UxD zo+5e)#LYNqm>J$;1%#sqpJkr%)!B&hSwtU@_K(;fM-gyUAfS}9n?bZ{aj?=>urHwh zN}rvwu3XhqckRQl8CA2pnOU32u*p4oFre_dK|L&uSKRf+>AvO&X>Ni}`@c}*W|zma zvl&+k)oprNA0A#G1VCjq)fktmqT7G{z1E=`h zxKw&3eOajJKWMr-SKYIxHDq&`5ut6SQIUmf8WsiGs-r7|)` z-p)#xm|)suJzkdO4l6-j(4z0Au~J|Im;ZXidpPhMe;pm0F3LMn%DKI{!1xK`L!O+V zW?V4A;o*4@voyLbFFerl3IU$a zFFbRRuP!O9KQR)H!tRj2&d4i9Rq)sZ1KAbGmn5 zc|gty=jyN+?fh6iy}GjK;MP0QdX;ew_f`sn5-+sW&MJ9!gra zb*hDYY;E>B27_$V)&zL2$91Y5=k0y#P#ug2j|BnWD-xUL`o<4Bg}gNx>6fI=80WzV zxI>3Cx*EAx6-?6Tu`slH+PuvFBGa=$GQHvtnJ(*yt}&Dig6UrW4NN~Ku7_+v@tU)` zo{6&d@aW=y4V7LJ3u7J>+8A1I^Q8XR^uwE7BBBTF$Z_ zC^jDay4*b1m-i%D%nIGSoW2R+@-G=qh{ey7=*RUR@Gco>p-k>gq;H`{FZ_os z-w$0L5-teCvM*&#;Cd&QbYM!pN56i1Tk{g=j9K_pF87~wIc5GQGU$Kk@}PR+#kRy4 zmj=xtsXKB*{TF+3czcg`Wnki7YNJIHbEeDfqSvZPs-HC!F2MPaUftC+w>>PwU2Hjr z&G|NPG7O;z1{19_`t+3~IwhbNGe08dY-dzMxvLkF@gPh!(ZV()|D~Syq0B<}h-COP z#pw^dyK@_l{R$$*W%rUkbE9=xyaoG`lHpM&@AvOD4crgXh*F_R=8b+&!{0yrn&IUc zuC#@nT;Aq`P6V744Qov-xNO{W?;h7#=J_?}=ey04+0tY54btV{ZD-ljJ)2RtaVZmd ze=1}n4a0FwNtq4ABg=}2Nu$;#jBX&%Y*zmRlLs;?X%hb@Og_XX|G$FCM-QbRW^Ms- zu#xDFoc0AE{FIb|p=U9Oo0+t|;pEnnf|)lUE$)Qxm)yLmgmjp!p(0l1t^+z(PV_xr za#nx=*pwfs<+6dFS!-imZ$1XjlNvbE~e|Ok$Bb``PYb&iR90v(7E8`=QHYiNas}e!ETnra@$JPOA_@xTHhu&+*rZ2j#d! z7cGqNk`66v#2X7o)B~bxY_EwUfBY-{8l@6Am^`Y?2K2GrA%BYc)}7PJn%L)wJL2ITibydVmJl zJs2O(-b=}X|8^oP%=&~e7OV05llLs_0I{aVZ>dwez7igK0v{!Y8N#p8@V{j&W6&q zV4&p7NIR2s6}3TjfUcnLPCxW%fez{m3}KdPk_OR|)F@K$+bL3E7Qt_$!?VXtgR&*w z3$`LTx~>7U!Yl-M=e1N`yD}^-9g=w+_$?aWTPOEJ7QYey3qD^wKCSp(U2b`PvZ|Sf zSDO56Ga95=7Lr|8mHnq#_Y37voG7}JF%zya=73D{KIj?SrdQrlCr9uUkY1P{^SLJ9 z{vt0Mh;L@Z3IcbgHJNx1A7#Z%D{n85@hgH6NR4W)3sJ**F6bq@jnrB3OIixb;3dW4 z6OLg}xEMMrv15>8dYU_AF+i?5%^k&U;pvjZ_Wg2$uqpU`in4P2UHrmR1I6I*T{?YN>S{W@I0a?%wwEFF7OjAg9` zmGn$EjhwVDH+A)It(OQ{LFbK45GMC)Y=+%rRzC z%7I!wz(DM!=~sshjVyKzNX8zUoD6Z`&LuS?WQH8Lkz~^ay4u_gPj$Ijymgh{wYBmk zFGWDvO(ndI4`46)NG%iG^4`Gj-8+xYMFxrVA=?NFB#U&}O=@EcGB{e7vAM7q#!6d` zej3{CQyB7w`47iWqmYXWFaipf)-Kh31GIzj)hrlDK6SPA#Vg<&@=E8ktpE|I~Bpko9a-4l70OML6tdn`Q(+q3;q|&D|N7n@y`E zXO6L>5%DcFQnge@RoLk`kLc`J)6LgXafrx^)jaKsX-yZ5XeMpe>}qF z8B^ty#``0Wv}x;bA^DPw{nspHHn{6=;XIv0Pbr2vR&OJNtqk=(NFFB0=l!qE&UOki zySf)j&txureV3Q_5Sq=yHRR(9dg$zHP_+ZdNtcCLr($mxu`|X@^Jt#vdcm+1B3}%} zB_dol@&!CgLywXFN<)8`n|eR`x#m7C>_5ODlvmMWtzXW|4)s6s2^G`(@;P?0RRT7JLVBHgvK`q~J4~_YQF+Z5<&LQS~s{obOec zN-zofkH)U->Rv6>5}MFnKncPagopxaPJKW7I(kf+p6_CT?DV72zu4*JMXNvTbPVS+ zJI&toQNu0&DMIu!odHHExNEb*rK5@lk9}{^09Ay5i;~WQr%%63fn7>Pc8*Xo* z#4&f|+22}2u+z(LNf-1CSpcR`?=@(bA2=lri?J&pRNJn<&YQ~W!x-JP3YDN|6ExmBp3H0IS4J2ECDXyoxGftj0eQHxX zSRu&2`ob!J`0Wqi(`;K5^BIXzT8W6JOx!gQawyijOhpmRU}bA&l}{%J+$r$|S*f_p znTBKPqL;W#iXJc_g3+jTQfuG-lF0`5FaDCr?Ud=;XLi>U6CI;B`7fxU13|WwoPHdN zxsDVfP$fT5rO4&YFlJISeZUvN(7fRI>Q`n?^ni@r4z92=6zX#i80x*J$1oYG^=kH2 zC=E6OWudFH>AE>*toc~9AWgE zd%EizHr3Y@aig>j#l`ksqLyEOY%Q##ZJ=yCbI%(;ew=*1E(9Jfi+*3j{R1Zhf9d3G zT&??;|3N1I2bi4u=7iW+7dMliZnX01_nTid^Ie%nh zAh$B$0VFU|Wu?17+L-*AjiZiqD3InHlT&p0z4TQxszF!i)Sa+nZwDS#VH7hv#Zk=J zm8w7KBLLC=$F7MR=^)8giUaxAuG!`#z`r8sWi90D)&#P~C_(doSYupK3#iX^N)2Txk|_bvLfkYXIeCFK{#QahETTWXu3HQ_vI7!+Xr_>O^^#v zY^Qf@qGR_B#_FJNC7wV}SWqWJT@W*Vg^M3N2Pnp*yeGd&b6F=vne=zjJdW+)%@r=|#X#UQ>yl}ntcjfLB_=^e4V9J4*uxr3!ggs{HD*FW- zKQp=yFfLCbV>lRIakVf@5fSz&z@P8+Nny!M>jlUO%fdB-obX>vSmxCJ6#q>{mfi{( zDRhND-}B=7)7xLHD$e8nI`wM@uBqjqs+Wg0YBXG1L)_d!45du3SQvMa2S$={L1DGJ z;o=!TSAtatmW_a0Sog9ydcWf*ZBDM3@wx`Beaq|VHzMX}^ zsLaa2#>o8m%AN})N&VT7+oQ?9*Qc?iDd!N=PWKRbJf5OoRT)=6VJBxeLrE06BM zgftTkXL*^`cQKRsVyUypYEgIl=+A299Tr zCM(QF&?%59o*&F7#)a+;Nl|0ay!GLCzlH)TiJv#f;IM03`Pi8c#$p>THfnugEmp7=_P(+i-X2xTAGNb5>>|y;rEmFh7jF&_);{mHFgV3 zuE-|y(SA>t{h_+jraNs!bhQ${Fq*!aXJ)lAtA&BQvt8+0WqIQ~Hi(UyY_NzHl`-2I z(v&tN$LIa#k-|Q9>xTTlP~{VCAXA^%Ham-C=4*hdOxO!|xy7v|5=5EHsbe9*e|kTn&-YI-BO(Dt0Wy}(orD}38{l^cR;lHjjv5_ z-p)sQ#k{LYSy~w@*-N$!*wI)kkF$j49^_J4S%0=C<1tU~dYaB-7w=T>dw;gs#^P_V z{moa!j$yG3Z}oell=34*Gc@!GL9KNZ#V5|OUVQV5=)(m1MR~Zxsj0C;ObwWL6jz2R zwKq`0M4GCbJ$S)^Vmupl!vJKDgZAabz|;4`rnT{_aV!Ud*X3-n7umSftPGP|+ybjV z#5AmLI7hI6N3g?CR^HA11OgWyWemm$VOlTSI!r27*_u=YcclnBUGpnS;(X#11^WMn zSKds0a(j&V1uUHM{F}V;f8xsX-=DsYzMay+Ssi`hUo~P)g>dW!{{wTXm{(l-+fRdx z{|#Ly(37xwbX+|=e0(`DfG1ota!9^W_K`&|?pHE*0J^apQc^eAI!)B8m18gnVeA}G zgI#9!K^rc3K#GAC37so|m7+YNM+D!k+za*ZEHy{}+~MsTm17uVZ+G(&+)BLCb)nR5 z2`^@q1NiqyBwZJ}iDH^wO8wim@ z*>0vMwoyL5K6u@ZWR`MG&2FaT{~cdv$(_h=>ZLU+5>kjd51mWhBZTsW`JDxouiF^N zQ}ZFmwI~K}0pyWI1nvQ1ngsYfb9QO#kHM3ruvGN$VBEw4JGjoXeJAvIJezaQy|Tau zWm{I^r|h6Sm4tqoRuYy#nl)i>g(3bHdFIj}BYY4}c}i;=W%9e2f3Vr1#->d{27lC| z?Aco%RP;1=7`N-OT(nM-mFuSpLFfSWd2@3esPbZUfH;4KXXD}gn+l%G=>lUPC%Jm8 z&*j)3@pPlPpmj~)G(*fTs+r~g%+G=i1@A)0L;ex+nGmKH>d+v6W`qOi!qWU^rIX7T zV!ogZ|K5G2ia`)G0{M?3Gju@@SN_*dZI@%6&!)FU>J=YN_Dq)V#XfUITl%daz=kFD5`Z z17%qUeb!;F`n!ta(_l+VCHzE8K+DAPRzYs&f_0HRX_HqW(l_3b_&ssopUd-bcm!!wA%#CZL>rJqr!jTz{l zg_nd=&%(=y?41+2%js;dyRFr=A3`yRJx-%isgPDtCr6*ZwVPF=ROlvWwDAb$j*fJ* zCBi!WL4B3d&ih~Ldxq))({zt6H-R%`*{*bJE0G9nF*CDy2L65-%tVR@n#bCO?k_pI zu}$+4l~Y5Gq$Fbhz`lXbXfTq9C5LZZNg|e2yLkqP+ZL1B+)6kvD=owj#!p)eW_{a? zb-r1I(;0dlU?RJlW z&QXR=6^&B7$&Ts~=Kbu6B}qh>er}uXJ)ah%*OqnC<60Feu`2OU#vTV=)v7>)OR1Zy z9sB)wv9Ek%Cf@ElVzQhylr2H~cS;?pTW>;bUZ#8;I>$LYi9M0a&6!^R7PNvL6#ZJ^ z*u7BRtfmpgJp$NCsxo?f)mEjO;wxVg8^zsO<(cFlYFgxb{f^G9!q+m3`y*3E=W1=S z@;8UB`^%p8m+R1dx!I1hO^xexd)K~?*j&qvk<&{-?<#$IgUaY~6L~NgDyh}Jl#zK3 zxv#&=jdj7e7KtgA4PS3~Qn-7iq{wpIxjLu3T~hCNITr@h`xb_02w>W+^JL%2-z5>? zMIGCwqlFltpjtlA!g6FWez~E&IKzB0y<7H191(Ask4S^lJ`}o8;jzlK!1on(-O7-$fMOlnze z7k~ojB?!}zVI~>;vI?}W9menK+_8kqK6#EY`8%mkF(^RHng`=C_|uxfEn>h-62K^> zY=~_k+$=y@-JK~4)ej)$IToBJzAAzkK@TSmi5tKWPE5lrits1*w|;s7qXe!vWmpM( zJgi#B(5{IXyyS4OI&g$Bs1RcrlZ28tFphvzFa`elM&t?RmV^Gb{+lNlvHD&RVB~yC z{2nlmxIF=zD5QOt@{vsSNo>*l5-DD2t4Q=p^?H^)^KN(xW@f@-+2Q|MNI+N+@CN$uq zpGiQo!jX>E6$dTGG!wd#5a(|$_^WC3fg3)Z7b^H4_y?Sjlpgzuhm56<2)Dz2_!u%vKzcXx;2 z?k<4@C%7cId*SX@g*$}c79dD)cXtgC+#zVDc%Jv`?zQHdp6U5hKUfQ@sJi!_Tj%Vv z_rdD>5b;aOKRnR4u61+jYDD;J) zGodCEkoOT|g_Mkh%Xef);N8dJKK*A)X5Z9y<+ zZ(kVbg`9^TF=4I~7?BVB3&2KSd#FJ+eq>%of)wfQp-VqXJ+9 zzzQt!f}4XcCk*s9kL8RS&%d&TGXo*#f5o3Dfw*@YgqZ()K9^crfcNlIgsWvXom-iI*GS=lp=+N21h6Z?B;6qcVZ#wswZx zkNOc=o_oMMF{N?#98aAQiMh$EEUyoXPl!B)0LUIgb$ph*JPNfJ+@7i*=IFOP(RJpm zoL}>Z#MQ712S~q>p5 zF0kGCzxv&3S$xeSbY*;gcx@NhMluJlnu{wkUX@YgBuKm+ctel_uN!(a@A~tmG59$O zIZ@mfZzxY{Oz(A0G+oY}MHHY zI3MWA_jI9uJCXKE#`9b^8&t_DkYfy7NSa(I|5Q2|H(?U5UKSg#(EaC%h%LH)7md@F zKJS-uFQe%>TXooO<2iFhgl#3kUiLIoIz~6qC<0E&mvDyT!86262l(q6#R2aDqEoS= z!vR~1IR1eAd;Wnf`K&c7zNdejxD0Pj+)d-Wf!)V}v>mrT15m_1R5T7XaBlwCWxwEO zw=F77P2gOe&1=4ui@xi7-TC6XH|cU=&ilTTvVeS(`py)B$N_wBNu-C(-$}QA>@`wP zzuI`{`<-cXSQLg(-B15W0>OQqlrVs?%DmBlZ`#QX(DZV3J9jL(T=Ot<{@A!&w>+bv zV5Imt4i6g39Y%3$GgYqa9SyBMq(VXGhOY@ONdaj;Cz;_F{5mH3&BvLc_g>7#|KmTB zNWq2A%wBre3fB|%T;ma2#o%W4A2BX%4yujowhfQDD3A@+{wmvtzCb3$$^gv?o7t?I zZ-k55TF8aoMS@{hCG1k=NF{M1O#|0sM?lRfTjzjv$crAf1jw3qB#|2cN3L7Ny`)~R%ep$77u}1^fDjVGPV;u@35xb>}k_EJOd}g0@a^_ z4GHh&fPFU>3OP|j3Au!W%n5jsdD+<2GsRe9&Y^S8;ANch6-$U2w?~I5X-BS zHvX&j71@7TalQV_ihH*NhT92ef-PNsIlcnjH7;nOzWr(C-H%j7jdNPqPoGCGoza>dnu=}AKhldPIAfKx%11|q6N?|R$M_r<>j44uk zAXRt373z^GGihcnjHGxXHO%WxDHXSTNuR(-b$H>mpe)pUoin@KK#oj39ckW^M5iJ~ zh+U#RYvh=@OxbI&7+E*-2H()#bUDoi7frR&lM9Kubicp3< ztK%f@A7Ii6?8W_%Ag^L#js#iAr~!)$m#uAIU?GB{)cb8_IAqPFFpAXk~$Nck8M zMf~F1ZwNW8*n8?mgfftb|9iz>k2+LQyMnjq_ zuX40T_Ab@i6p>Gij4UxMq z0!oAcMTDwEOF;OeOc>UrcplDfBQ#B@f;XKUqBC*7ZIvOgzVjTX_XIgO+#(-Y00=D$ z;Lhtd+fa76uvvK#WJ{v|R8?5nQ>h&d_8S*>Wy1HRQ@O6SgqaP6!6P4ff3(>fIAmXa z{o6d0GhLml>B6YL{b9aC3l{`iY-@$<alX3p z)<7JV4=>fudaX6T%HBvwVlBBlE96{#5UMFN-w|x1p+F;RDZeIRN%4vb7Nh#EGCMqH znO7tC51vchhphe^vq9vQk^T(9Fd@G>|D_MXx7SSgg;;z`%7G*O!Ac6t&t4vBR#mIM zh7&*zZHIwkTikXnPJm!Ek{0IvNmZoF)hBNHkyT$saOZ}Q1E!VIYazqN=(0JqUPXwZ zt)z^1dhT(wKbjyoV>=md*)HcR?cKqgdnC9=q$@q9KRq#Ex$52OyprR^79GFqq*3L~ z{8Q?SXs+~kU9o{=IYV#^Nc-?OXBYlMDH(7QOPZ88*jz)_+|Wkl4^KCKTI`ynFoyAiD%%ZXRIIsLvr4!cYF;;Z zq@d0XaRLK1U9OEFe-n{3H0QJHTds{Vi73KPxM;}Us3SOGr= zxc>sg?|vL%5pxXD9TJ-}<0THv*v;It< zmXlRM1{hI3+T^d_4Ua8mh%XMJs|paD7`+0 zO3hq0rsUz5IW;kTjhBRq#2A*Dl-&Mc1IdRJ0-BjzY^2%}6asWfQ#z3uVL##YPpDf~`#&NL+JpP&G`8x8UZq68G&Pbz(yC%XrMmB>Xr_AJSSYSfh8~Im8_&#n$V=ppy7Nfd#_UCTm9}qO zrwvF=Pmm@_^nrM*LW`^A&*gQVE1n$>7ys4pQh4J4Sy36~6=@sxjv&R?ajrO3v9pXG#!NWkgc|pwre9es z4`xI>5>2j#t!Fp!%ZDVddaVLKeS~%qMn0+2CEb(RNA?{R_>`YDlo8jERlob())u!h zaOqrocB~|RySsAu!OzRT5%>e>lINFJDSH*iPYRK$=XwB3N(P7asmR1s2+p&Y1P%4h z^Bl-(#4De5E6B(on`Wz-$TNWFb&L89%K_>4gyPR3I+%(qiFomFn=mQYtmnR4y`TTK zB`amwzYKE@9L8YXPdpzw=}AGe(~ZGxx;KBcXV8(AOPt0)jECCe3MZ*1QC*Lz(Ne}g z2yjb47?uV8bqv$Y95er;t9a$M>ArMflurhS5o3%wnTBG;SA*6&(a|CNOX>|A>>98OKq;NPEVGIRm*VnT2-6E$h`4@I+RgZkEMMHzlf&O{YSM7$X*x2ILM0F4L zhd=WoZUb=yr%y54_9h$3X>*RHZv-UJ|3#zyJXw&x8o|LZ?nRyae}5I91mqzzawdw5%OL3h7u=faPiPRb+F`QtD08XnZWLe)F;dNPF!0eRG#cpx)dNkJ*^8H*~QEQl~X-jEjHZ7EM&+M{lIgOVP3n>qJ zODU5T+dXYC9nZFvxU_MX%ZUOI7Ikb)9M<&xQvcxUp}5VU4gL**+`0rOJSo%zxxSNz zhpJvXQ<&C~zg-TI$)=Ra$~^va9#o+tK~*v}Xc~)3gsYxIw_Q zvlHLBt!snc4HFND7mP4ZcsWP|dVO(!x;fa!-_ivPf50qy7WopX2_3d4Z;GgeO=Sxq z$cGbCXV7r@&@ZAy`Lhc+pD5{+6vJTAVbERUdyl?6y?|yfyn*)o(C93bI;;`2o@pqb z&AM4dluGDYQy5cyGrxJ+wG^#FKD>ELIr=Q~PG8b!v4m8=d&Yd~6NygXlUGt9^zqF; z?nDF2M2Ouz9VOvC;=V{UpCg6GQCgM3kcT6q8KgGN! z^y>)BDGA4OB#XE59bwH6y#c{`(810LCJvuH#aBDlDKs1X5n#3SOIwmmw7r%0O6ixe zQ0Zm(T}}D(K@Sg~z(BAsNQt9P0Q&b{hj04%P4Wx{^l#ZEh)kExXZdU18sE2Ru~Ubi zP#vl1x@b4aF4&)hi>#|}k_CNWI|#M6x zc@iA?X~ns_X8TlG;>zM$$|alCAFEfe>mvn=6GO8}V!{w3l7Pk7Imw}<11_D&v{VTM zNUi!Aj(MS^D-X$(2trHAfkh(f5gn^uFxQpzr_lp0ow!0RF zT&UTgjY%zVW;NL<)f07K%qaS80WjOVcJvJ6&JFjav(jC^%LMKhB_PB;lFVb5c5ydfXy78;1`#qZhqRzdx{rzetA zMo~}U%Bw?fT!~YL(NaPKIC{Gf*Z{hB%NmV*Xj=sq)qXOo?Rze2tfh#f`v-HRet=+( zn*YHZiMG)C8}EMHeoE*zXzoBD+S{{CC4u0a4#qBWt@c;R^yh1%u~!!;ny4d7J9PUl z;s3~J&2`|AP5?RJ7`i~SP|-j$LrA~&2xCjZ^ZevQ=vd+A9!DayD{L^ejNj+w+5^U0QzI`y`z0NdgH)8S7Q>ybtH zzP`ZT>|M?P7yZ>$kpP~TKoc91N*Zw2JfyL3*UUfH3t3biQGxdg;ZDdlzHQcZf7wq0j7S`a-jWi` zmZV0k&`tB8w4|h4xif^(j%Yun$C}8xNxl-F(W)4IV5s$Ept5;_#^fX4LJO_p%X_Un z&o-(FBSXdfn9CUC*yPB#gGD2V|1%B)f;m!!4M-4!hP}5|zGG{W?*SbHtzUvRqP`I;s{XxZ}Rrb)GAo$|g#J5Sh}B zw~GYPw+$%P5r8zIb5sNIpRJuB>^fqLm|>x{e9wt9(`l|#`K_BG3>vSaK`qpS_6@;` zm5?X2ziX)w)VV{tnDcX#hHtNh6v>rIr4#bd^YVZ%pI?EmmyoJ>7s-5H- z{>-*r;XVpA`@NHdUvwg5YU{`Xwx#4&W00?0n zBW|DTNgEm#ke@z$^cFL_@8T*cml-Mft(+b;GE}H3kJV%@fQ&TR07=VBG(&Qr`&TA9 zJRs50!klt1)H<-DbZo!5Kxnm-K3>ey-)g5ipwSTqEhLHlHq3+U^G;5_zmh($wEC1o zfdHUo7ovTDxn#T{pV-!kA^k_H)j*kfQ_l;%VpCBO=V*}v^zvM`1n#k zGF6{&miP%n+kZ&M1~0W4dL>t0Qzlh1Qq>gDPuPjHl4P(y*7cL#dFm0^3kVHPpD?gH&%0T0@6i`l+9Ti6%HMp-Z5v!Uw z)qHUzleh7Q@H_cz$LETTmBS>KF3!li=Y zpNMe`_Y_3{4=3t;CKrG(!SS4crI0;-ypu?{T3>{?;O)MCm6Z+a`Eg&B|)pq&$Y)!YK^U zC6_zQp8?PFE_W#gvn05;G~Q2Qkt#I=#{9;Big;g*UZQd2lf18C)(%DeM2t>{+p;1> zA0j`j<^T{;>Uw+H0aKf&`(xD?&x@I#j?fWl`vtDzJ7+uP6NF)#;Htavw0U4<5zTsyRGEwsdFrOQ>Gpdti#io8Uh~t zoPI!5BHhqFf_3UMSe)<-#Pf^c&xIn6}bOg90klPt8(dT&NKo6gW1 zgVNEU<`^UBSU~LCX%=gAsy%t`uzPy_#RZx3BXO*0xzt4ZKxk;b_ckFCn33+qbRr|j z&Z}iPiB+bqaE`M{Wcu}sa2DaAdDY{b{f#l`2^AQe4hPRxlxT4z7m29)3KeO4cE#xQ zfwhOcUxEc%1eeF;!y+6Wy8tU-PRmS#yj%46-pxwm`Q)@!|o}3ny6# zHDi<2pAbHPb(3>%4sHa$)+t#auq0l0LVDrk-}8G9BJx0v&TgvtJ{T?=?_5K(?`>zl z0fYYtBi(F4pB0tpt;>txu3DAtwO=O^NLYO8^Zx}L5vJeAK{&@U0e>M}czJj_INcwg zOgp+{HvwmD&;W%OF9h-7$~9(hVlbY$2@=z2-6Oz-mMtri+>VEc#;PK zTGnnz%W0A5;m4$m@W_OFMijv3u=j_PD>!n~ma(*-AXwz)@oQsUH4o#EQdB%%s zo?mboD|wN%O3)27!LroOSW;V9lhcm8K!MC%(sWtJdV??DE7FWhJD)K`wdomjss9=91Sm6iXYhZ0Fp7z zR%K&CN?oR0uGTQ6Z=Xx&fSrVxcJ&Ht^Gn4YUsz!wO$^9T6W$$Q$WOld#D5x*)kH{W z_qzldK4rfJj0!)kxJVRxDe%RO80U5WK|FJo&$b_HV$VpFkxhnFg*iu9N)}Nq@D277_arKi}X!my7Fm<31M_oVNyB zM@#0mgZ~#-;Ut791jq%$8N>W&QaqldDXS=J^XKP4u9A`;BwTVCn#L~Q+N!c!*{M2P zwC|0jR=4VELbwD-259Ng75@PirPQnWE}IzKpp7o*-#X$j?c3m@Paxp;GvkjKY612z zgSd$#4tn*2slrRCCtRkh?MipSlv0m+>YDZZBKfPN($eC}cOoduW7;6$WdIX`qoH^( z_yZQG`2|2`@!R!_RL#1wX}Zj@X=*INRQmpxE);AE&A(f8-$Mv4*x3LBEfHL|i#eM$ z;@?a=e6FAfwT!p_-;>|5LBNEKq5``PI1Y}l+E()~~3y38VcLAGnJVf)D zrT=3@b&c=$IKJ%V6K?r-x&F?iZbo;E@JK62zZJ-8Mf7_XGb@iZoh5plqjXR@Rkq88 zGZ;cT+OklWRP%4N#!|Q4`C!f&K$|#Y=mwnG=X|=_9B(4Kfq(0PzYVr{C9G$KY90Qc z462(Al80d;G>!kf!<013xb4kIl|)A`x<^KoKV*@%?wIo1*`kHscZq9_-#lZfu+EVK z@(Y-E8Nv;rLAm#w;nk07Mh!V z(#z=C$b&)Hwsc_iOe0o{OBX+1 zU&o@m*Lc&i#W@yZFCP+1BtK$v#`uo8;Mds?OnBxdREP)ZDaEY&Yg$dq{ArI}r*ajl zoE@XaQ6O$mc(a?n_S}p0;H_BxYd39)V8tLIsW^RV-pxU~K}hUZ9e1>(to)@5vQ!o{ zk{KJ-CEz{)atolbNe;-hAnhw;h(NKW$R?*@`xW8i2L9_eh1@AE)jup0Iv$7{^Q$kj zm?+ym%5GRH;>6y%=29#BIEgeDgdL!<4lV7h_F=^LsfF2$HJ}qAJ2Z-*H-{ zZ$&(lQgR+3RxU#XO#yVjtItAqzxJ`S%wp>b0;_g7saBAzClm00fPGZg?NMX!2! zTo+p^*L2dCzd}^n=>plpiAG`+*$(X2RUy}V3j+ZMKa6Z0v&nuzBUlg%bcF%*z-tOY ze{P6_ScxR*f*6Lt0rx3N$|)2{w$XoY4N9rGk^f-@0UMahuMV|iS#D{6y>-QQI9P!5 z`wQn%!?DTdp<99}L~BF4WXZ=fTG{7K(X2m3#hL~h^SfG=e$LpSrz^AyE^n`I;&G9? zjM#L^09@dF{W-@6ie?1Rbp@uN%gmrI8Z1-(R#SeH`!Ku^VlCghHh(vJaT{VAI+Jy4 z#Q%A`qC%;rP)xUC;q#Y;hh8Rw?6POz1cgY?uzAJnI`AB4-LW}*XsmTTjf7&-?S=jr zU+z7szAL{U?dM(IGQGTrvc70zZIvAO+s>&teM>`be0iB2`D^FvD5_kVf@4`7)zO3=p*pFR~2KHD3sD z%WcGOoXoK@Dt9jB2ho5MU}M$6_Xp4yF+8-cCVu*v!4TX7#(EH|%ZXl>mJpQH9)Z)O zxa?fFANMO~>ZeT{FFe(AUqM@6&$P6y%l>O79`Wmo`<5+HXSUVPTcYT)pPR|6mG09u zA5w_MMN$q4EI4B_fVmgH!V~&|_;hzE3bm(oNqzy}nRE2YdT3S;*w;JJ&+wX@=YKql zZIFRyq2%$;nP|SP-x)Bgw3Q!?1vJ=pmaUAlyl}@yFc3($P0C}Z*6evDOt*1kzQpfp zX&X__7j$;eixy8;0Xf>j>PK{T-&u4B(mKzq9>U?3)jJVFz}(R-x_3$L9ytzeLRV2s z>duY2?~^JoJ(Jag^j*wyJPc%$HrewsJ@vEwXEMJI=_%YoI%*}zjWqLICOFhs6h=SUokgTj?UrD z1ngbv4=YrpyhpSuuYl#R6xmnR@*wzXr|viC#~05rM7jUK-y zzRx-3#sYrHEgs9B$-?K#bja0T_=5?jq{#n33f$mYMAsG7o#A*!nKq17pGm#H7fdvJ zw`1NV%@vqWILhAO$R|%OM^d@Z8I7cS`Z(~tV%;ZjJ(ogcA^Jsg=>3RdB^zD5 z6upfoz*22umSsdG(AMKeEI}?iPO(bvjDCav&o$rbd7p$IYnyrk*RE^S2pUs*pQO`b zae%3bFL4fPd*8kBG6^`z4=GX(@ zF&wlyReEd4Wwsks>K!&$q*Z)}%@p9DvH&ifq!>^d4!wSsx5BbuSr{Akc#oe?$%#t> zQ4qn}m#(R!NN2GYiR0j?M3TlZ!3l6}W1;K~|4!t|5XAc+uQH`blx8ZDCbTPor!<5j ztclyNFbB>d@EQvFAd*+bY+>2Lt&=svTEwl6+HXP(>MhR`*k9}Q?SM?GUs|2 zkYT*ZeeO^#HZ6<*e?l>R6=Yu(ku3+z@1cY?P0LU9K*3j&k12)kd|7VmY8r2Iaod4mk$k&<-p1>5#=!?cvE^(PMb48 zGQ9AME)2bl!d}$hps$~Ya4t2DH2F9nX7}{UWuzpWadeyZlyxBW!{aYUnz4R;4+eBKa;5x4)aVDY}rh&7ic&ZFNCW& zhXb)?*VTwU`7uh2eq9G98rL>?)8wr2bg;!-Dnp^$9Sx=1P5faxS0k(^mdhElE6mLiF5v#}7t zsBe`b4Ex$Df@y*$89=&wa_q zbJsGoNYJ@|*BC$tm9hbk%r=x9wro~T1=$j>D1TYOBoirN#c9=Q|7a`oUf_1ImBF*o zQF;(oWYo&_N4JlqmKFW#=3=qdQMUqXN%cxNHB^iT(+YYT zDiUg~F=}UFU1QwbzKJxs0+iEDFhduESOyJ|h;rZ`ys&@4XT~D5`j+{@g>$SA?o}IE}*;A`-A(o0n5G4$4GOX!%kp&XmjcR6Te?#x#{W zJ6%+*x+`kZZ}?%&psu2)d(X{mFnVwJjnU!rt20{q-~wTE@czT-gawb7D=hs_NatTh z=ces{GCFZ@j1G{SKKg$`I)CWeK@Eej^$YN=Gl7v;tYnLLPe(9&%PhgVNmUC`N+v)x z(TGThLuk3N4}g1}~tj+#U%BkZEC8bc;GfpJ;&fX@r2eotFxI5 z1ykyr!{l7yFJ^UIle~=?39ZEf`91SD$-GGEFOZ!A^7lb}nxsC{tdXHiT~Y5FEw9!8 zgXt9dD6r)JH<-@ce_=YjH8=|WCv&U!O?y6VA4VthkT$+vf`LpT@(nC^C9FUFk?9$< zMbbf`hsW!?@J41FOD^b_E_yf9*s^Ftf2hXhr$hJKLDT{MdI zV}6kUqjWYI@QjLpiubfLJ6*~avt<2(NRn4I#pq;>dmJVm3o`BJXnsZ+y%WS3ifY7s zHwslmWU*XsPLv(m@A8nBA&E5{BQ-vNL+DmV*ci&HM!dB{IOioQ?x;#%URxS6>!3Yf zql9J!sb^<4ZCrSOD!UL6yG<2ZYajkv%xc%T)$slVURMjV>oEWlZ4+OGiBq`vh)39l zzhf}PLBJX;+p@pPFNgPVTqI3aE>vd}kSVXb8CzO>Zb<>IohV&WZgPxQt!$T)dtN^q zmU-hMj?5B_1l2OV!*^$S`kS#hq=a&=GM@rEK3Ch7e6oEjXS=V{@C}8T@tSBG`}D3` zi!cM@b_2smP3hMK`3P>lZ@$l4=o%4shv$>z4_O%E$EH1(X}c!sAa#$ZSY##}|32;3 zNFrX%MJ9n_Ew!}FS+RXlHduh9ASxoCS|C81S;X@C;kEhZ3pf0mpe&(1y4Q2TrDVBW zXNuqS`fq8z+|?A5;ty`lMFMmET5mFph(jQGA>AnzFK8o>_U}G8oSx!dMG|)$I&N@L z=vMz~KFzNo7At>6u-eemXYk|(?Qf-F_9xMtOPIZyvf!bocs>cUTa6Cw`(>@TMf64D zruB6>$K5{yy7%rSd5_MgH_T$~Vk_e7;;rgpN(hR2Gc)EE*Kc~V7@()h z32AlZy?S%}BE`pm%K3Qo1)YFJ+=Q{8_e)=@mhHqxePbzJyIB~=ooUEX#1QBY^fnH< zBIaK`>C*UCA7e~aW(jCca$ZIYLU9d=``Eo|+L}HKw=#1F#|B9Jfz}{5Z`uAaJ=gI^ zsL8_lO#a?0(eUQ)N5Ya}k?@4HR0$yenixFxEWaY7-Fq<#X(Q^-p+P~lxf5%5SwT;flm_lZb%TbrLJ^8$}YD7T5vJ3{2>QUL^ zJE=&G!7#;nUD&U2#dhgFw~%hK@Km*icVFqc5`C_RiEG(kLFQ+#-oGfzw70aQT@UxG zcO4$A*)QO6Xt&w?gz5-$ZTzh67t6}ld2 zEAZ7O=~Is;7}`KnzqgG{GK8lo_WLO^)X$?ow~nb2{NuSxR-Fxh$ZV3Ot~K20>Qz^K zRY+)K<;C0Ct=*Gx-2;k!ghE_AT3)PrMg(eND)zsJ$*CNNHGK} z>4FKWx)`D+)bX~{E10M;RT3tLxrel`A*&jix<6eO<0>5gggwI{lf3>HtMk90I_VHp zrx*C%AK9fr+P`u&}%2-P$_eC!2|FEF0W6;Da^$mI6&m z>A{lCDvB;zvj0`TPFj-TX)@y&kl@S|4pHcq+(aChxrT;q_~#E8R=@je-1%BQ_geWb z$V6!rmmfQp*|B_Wj#fChTV`hH1 zLlTVL^TSh+BLBjXyu>s|MA)D%g?{R%m>7bJN)MU$M!j2GU;T6r>v-dH0RO)Mb_%rL zq-9AP>7hmK=A2e%?_u9c(xf4xqK=Ma3&sXEr|JlhUniw~V{Sm(=a2~pS?mfvQHxqe z__qmqbjSq#$C{<>Asm;Ozo5iy>pA#Epbw6c-d>51F!@q_yk-;QX z;71WCMpv{EPM|g8$S*==Ihh@Qh@p&pY1hm^O6TjCSQ!p4Ghjpxrah?s_gT>8*=6_a z9*#st5pmiQ?PaLZf>*S&Oce4ut_I)ItUV(Q90JRMjy2&WWlWa`H#NEj!23h)cHvp4 z#CxPt$KSyjMk*N0XrMr|wIXnuI7hR!A4lh!u|HHa6HvA6eL17afqp$E5o1piZUon2 z^Z4kQJ~26@9G$elm19;Ls^_Day)=0wJxNN|@UGDOJk|+Lya4 zr@$0Q&%m+pPqgebfZ<}~AaBAxIE!8&9(&51e#dS-YLSq}u z3aC4uOfxnsZX3Oo>w#Hcv=oB|QNG6Swj%V46oq8`LZZgDyktR>3#}&?0BZNBeinZL z9jZL61cC{jz^MU(fERTNpI=Pu``q=KGfScgVqoVlXrd|vUs>>jr}H_ zvU3ol!!}{1-_P(@qNOBUfj>s1cWPRWc{xS9b^50d3)sk4No)M!8PP0xdh&>*0WWl3 ze$WDDFA0r<_Z1)1jQJ;6B&&l$V}Ty-308|A@xkF|Rsq{BJ?Myc8=Dg!26e;p>7Y#+A(3V%M@qYz z`OS)x)9=$SXO4lK{7>C~vS1T*Wnil9t&1R>6Dy9;!mkNS*Av7LoAj;?6IhxlEV(Sv zv(FQ0{?H=tHdhk?947$MUF^X*mN@@<*X19^tHN;cOl%KF6)v%SvK1}ADx_R@LVz_e z+=e5bYFZIeu)OR|0fc|0P4k@}Iw6f_?k9rK7PTK3lipJ%tLMmaRILcMM-ybLrfmVBc z0`%gSzl56Io$?lt zdyCtEy;sD9btq{R!n5Rz_Ay(KaQ4nb*|-~UzYCp3^1}Gd8v3Ii`Vf^Ty&uSj4HLM* zyz z{C{L`&_)^)Cedq#eV(og^x#qfc`0lo8kuW1X7qy*de(P>Z;085M!BSs&}gLP7JMEK zKTF}#E~*U?-jVB$f(nn)rJyFRf+@#^psIjseEK8&XD)%vzzN9Jx1gcpU_37WaYoEo z7+4LH6I8_XNj@caJGzKRXJtoK(dwEtH>ELZhE!EoNee;#ErX0t?GwR~l!Hyvk^vDO zt-)j*2&{QW=bcyYv`Ir{Kuf@f)`?F(&Z;?>D;r;OA)}X2_t(*I(Pq)dtJ3^dFJ$|M zIeTlg&E;!v2mPW5)eCtM-L)M>yPxENj^oPD3?k5|AvSqzv6^C2^$YhuM#)b}$ecVl zS-$6S40_8x?PFNy2KA^+cLr*4B{9;ifYt)Ag997vX4s~E9f{`t0&-O0y`-&K)aAD) z6xrwv3>+n_w`jE>%g8wv-u-udi;M!pS8mB%^m{+b1cnK%vF!p=?4ED8KfAO8J5pj- zkOH8?0pV}JNVxrTE5;`bp!iR6aS8Xl3Pqjw$@d5-;bx{;%8EK*>_P&jw0k#_Q77YW zWf({W`GA>T4{p>PKX|H)PXa$$Sg7X>GUWgAQ*t4#79lZKnReC~P55?k^vwB%2`U*7 zk?a${rI86|C@cUDmGKPhF-Z-l=`$<-2688nM6n%=Bifp~>?(Q2rPoZy|_ZbE=g9=brm!+SNM)73?- zz=kWAoia-vTD#1YeW78^CuYGVNCmmLA=V08|L^lyEw|+YM;?Y6Kj>kh`@!Xl`A^sN z>PG$vbQeh&O}d^Dg`FNJ);>n$dW95p`AGkUZXG_1e#CK5V!)`I;DtvWM3EE71-Z_K z)G>Mq3-}+un*fde0bqXQ>$o~b;eA&<6in049k9zvn4=hzXu65k`kxsC#TeE_iY3Vvgh zF&mcp7A+NvY}0W-#B=ng|7nY$sh8k_0n!X_LEr`&Kjb}uyzZOaTQDeLUTJShLCB8) zS{4X{*5%&V<~?O+SN~EL2ykjW6nwAv=Kl)&A>qA~1*UF`Ztspqf&JtPLR)~0R?xc6 z7$1*rUyrtTU+)s;XWs?lU7){>ej8M-#O203`7@R7z6qmy*76akrXzhltVQycoYcYZ z*^Q`UY)IRS@GMHtJoT5C#l;Y`0ou-pA95%7)jr=^dHJf7Lw%4qP!J#OpBEfin4raJ z2>jRZ1+Ql)-;2eS`t1-l9*7K4d{G3^kY-Gh6dFjFR%;UeZEC*04zN79^KO8~{YV^E z{yw6Laf~{t1(E6^^58c=G3mH7`e-5lzgyjElG>$`ua6J0#(wi2A-=hvH!-p|*+7f0SffrEizlxEGfSZ%CKvS|U( zMmnc^8?IFgzOeRzg0?VQjOO9|3^MQQ!@Y`ko80r!pb+oeu};dLo*v-Hc;hK{2qZYe ze)_4MUYPBn^Fba>SF_EMz@sZ66jnK+Oe-F(#c*eixlFzR^ zF$TVR_EKQR32`nJf()Jkv;>gvmXd_)rk>-+`!$4~5^bb1WV{f(0VKZ!AlHY`0`S78 zjvda%F$*xJfKw@cDB zW!;iLVq7LIq#ry-8TCl1Cb+BCly{BR2e@xq-xVyLm2fH--f}7ppg_D^@3!Eg621a2 zuvfcLyg=zt=4-v+GrlKh(CZ6;99VkG9)~UEHBo%gq1+}898s*J_2p8tmWtjrla9Xa3sBKtXeRg+qyM_7x55EBaU+9;QlO%&W zKgk3BQpd6VFZj#G|rYqPP`9 z*-uEiwZxXjY0m@Y1h~^vDrl*QHBu|KE#{j!cj4tgLpY3`1p}X@T!)oq>^t>{_jqZ1-&O{S1%8b8F7qFsBWcMNeQjCP7Srrno77(fRund!P0Lp2Wi6b znNdPYJ9f-;$}~bw<)B)|L|8dM4?X69Y)zpjX84KrB)|$%l|G!eb_JD8Gs8Qnr=XXfFT00qxa61*8E-*W6qAdu#jp$HQ>8%@ z88b;RDS}=rg@Gaa%bx|_dGXa|rLv;qJg)=j@dWupHN!8#U26u~_$jMoIFW>C%GP4T zQw(X}lXXJeIEcxzpOa>vZM-K0L=J$YL`dx(wZsWSm{tS}g zck3?B>Gs4~tWDfE0t+Xy-do3HxV&Vu-2fKk;ofaY#Qu^6c#{!$1_Z(PgI8_2AG9+b z!{yZ;tJgSi5>%8?At+w?zTvN%2I)Kp3@?{8KK=UYu^C;!H?n5IK1#@YStH1H+vlhw zt_Sp!x7re^Yb3y2ZDVcab)7%6(Q==EW*wjXI*r6bvy?{fG13tD9}q497p*GRdb%II zaISl_8`VG6Pm(GwZVK+WKd)mbl6<;8efG}X_iKAAZZ;_nEV>_!i8F8YO9Hwg6^bMm zF8o9xjacHHu$GI5cb=8?XWOxq7-b#VrtiY*B$_LnW$6!-!=RXq*SyRB-w-a5X*Tw@ z<{Ut;#N0NNyX#C(3g~GFXZEJ=ek^WEfEa0UZ1W2KP2aZVj63aFws4BOo+hDk#kTx5 zT~2vO!|39!(UHBw?l}%0feF7;DXOgJd8J?V^AcU^3T9lF^2J?rL0aRFm%&3LZ~;FG z`~5HxnuNF$Pp8z2IFCMf1nRn7O7qkNc}oCobsOJx#ks@98iD&bvvwwp%YJeLmOLM` zSM0ebZa=hn7U>V3pKTji+}?!;UNjP&vU_R4QqaaKskjDw2)n<@c#O~3E>5e|%wti& z4f(Ps&ACQzSGrB2uHWD_;jpFg;TJe|d)xLd9G^xdk1KjKg}AL#Ej9`(Dfj1)e7zHJ zjeoAE7caae$Nao>82qzwZ1u>YI}D5MD?t# zhLjz~gG?-larTr-AF6TtJO9a7M5->`cW+M{#2-GtRZTJ%4LBd|?z6o}v$V&1C0`I5 zSzpN)qO@1?#r$vb6iD9&usxe#_j*$ zFX1~p$sNq9tDtod>FIi`BZ+q(%!6QuZ-=(#!+lO+LIv=4VuGzqV(w_xz|Qp?4uzOb zP2&6DQ6^1t^kC} zG_?t^FtZ+<2ng97Xfl@%EKuf61;rm6b!@>J7GEV_L$l5Lu{@O}3dWmti@(RlnM%8W z@XY0fFMCWc5*x=0;i$Kw$BM9ZW#Yv$WT=+6D;+81jsKZO-<-Z6#i!Ic3e$sRFfroE zLSmhoNDn;4wpPDCvD*2<96&RP!iCTMu>A%ex2|g`K?`m%@~v}S_6VFXBBWRc#Z9pA+aIFtKKUOgHIzRSvAIlLjXYxFa zUQmv+i&|~-e4d>m@h4&J#u)LsGx_?gj~ENGH-nRxs!lfhRBR5s1V`M?``l;)n%U29 z-l@>hf(U+a#`}US_eW}V0EwiYM)L6$D?+4a^3sB2wFV&oK`{5@v5qj3$q%9(LE~2e zTI7Fi#RDh%f-G--xBe~?s=VIjLZl#HAgyseuSq&fhGKyS6ENp#=>iC`HJtC1NG?SZ z$}0vGW6LOzt9BGaRou?RKfvtXCD3l>e=oMt+mC?XLhvO>Tn`Y!jS?bnRNK26=V`*) z8h-4}C`Jt~As3Hu6=xsJs%ZeZ?5=wQ)9Rwtt1_}GEQhtQ2lev{!qGu;k?1rgXOma{ zvh1SWMTKEZ#)q^eh(P@>0@zsTpH;iDhoB%*fpWFx=!MFwDpDokoCAg^iqpqhc{(9o zcOhGFD@xm2nIqW7&P^QN+!Dcsc!ZtD0SdTL3gkH7`-|`HBoP!nn^@wVts{+qn~VA> zcX-OON#e??(vu~`2`VHW?OFfPMH{#XN!Y5A+f>;-hbGuBM>Zi-@xy*Sht ze3A=6nms+2^n8(cz*k+(HW4MQiSMheCch{E@88*K(Ay3H;3%PMhP`y2qHURg8XZm% zKYxViBz;@~Wj}juHJ_+F&h~=2E5jsLi~wcDdM}YgYM>@L?>}Ie_!)$CK3FB`1MJCI z92J{>TEhY( zD0m8O0wn{}=mRQ|>SF3t5{Rq!$VGTlLie%Kf)az~@0W5vBmmOIgegC^D0|hJ2T)65 zsC*V2hmn|NqKn@bP1r{@@$v(LX9O_ouUmhbx8$4ulK9o>RdEq(xzr9&1IljMDo`Wf z1)x6^2nggyTYTYn3&zoB-BC8hqob)V997FawWoFS z#~4YtS-!&&HzYgFV%Ww9=4t9>Kgx!)A;?{r7QD)8-lJeB@1^mR=U+bPZq+&Ik0{k( z{Wjn#j&7gGD%izStNlcqx6_+%%8nA*ODDm_?47I=fR%ffSijh{{6|%jo;ko}@;3$} z?o&JQvbn%ZHzlA9{oO(;^ahhqIL9ot zO1E`vfKy0v2|kNTn%WWf8@OIwSQkFNsgS}kUFq)Q`HT~*A|8*6#LO0ue)U-UUAM-I z8oog~5L9Q@kMebfuHWto6>y4&10@1WcmCG!wdp5ftO zJROA57YQ4?gMDAao9767euG}d@%EZUz98NRs1%dH`O(hG&-VKt62|uX9}?!hDui_i$2-(LJqS2)JJg2vUboHfL3m5bu_TN+ zD9jkDY>Ma3t2)IWNQq&HVP6}M!$|DHUlt&70A&y-W!n0Opy&J_8wbi z@~Vzg3*S(s4d05%GNX?my;GB?t=TiGHOF7fWTJ}|idne4oHt2Ci~{oWo=ARtD*nE{ zV<1R6<2xpdq|4~!mBfNZ2aORNf~tP7L8>~`bCt9f3+O*Z_k_A74E-rxpLv zV>1Kx*i^9&HW*y;|BJ>3@T##vdDYki{!?R9$arUmPx)VHY+znBHva#W#s>JGJT~e7 z;<5P`jZNBrPh+F3Et=uM^qVLx%iJ>;id6k!E#BBTI+os% zUFDKuRV297RY;2736VU6XV+}U<;vQr$gGw1FfBwvh5OWnBWcMIw_!|8J&Dp37tnvD zK*0g(6+d_f%Q1PO=7KiDmwn2~-TIM64L2^;kD!n6SMR!TiZCq0N~SjJGP**zAX^kp z%%jEWM*QDIOxu4XVu1f0B!);U{{jCG5kvJAmB5G49hs8tjWEm5?WnwUm0^wGyiRDw zu?S^V%h$S?sPaDvKYVqAzUA(A5Jh%w#)0KU-A{deVeHd3 z6X0abQ2?Qa@ki9~(DObCsYpL60hdn(~ zcQsue#%6~j-LLh{_AX_T)|LF9grBOM$z1}Hf`~W+0(;_^L2&XO2(HCLGOCp@P(~R< z+Pq&j0I7BCNzJ{0s<=veWd2EN=u;a|m^C)`7PV*@wOgXa4}xVA`w|vZGB0F<>2b&GCaOO7giHU%hR~QXNhmEB{92*ByTDdN92f8aPxq=Jp#xT|+S&*b zHyvhAj$$eQScij-8}j)nl^9dI!v;0&05*O-k+{zlS{BIH)oqtGdgh;c5=lw#^9t5lxUPFH9_r-u%X2IB^U?GShnlTNQ zSW_eoR7p0ViYcnYv2Owe%ddUH*pOXx!F9a#G-qD7+aD&MtcCq--2$XV9Ia6Upg|28`yjT0Yuv zqcv>_t=B+04LC_VX|&{lOMbnDAG8w%B1Bs<%OZz>p#rTFCU2f}%uMR|cH4&X_b;VK z+~Xh_dQrFA;&AC1Y834iHw4=@1&7`A|2SwA?d&wZo&TEp)V>VyPb>x`FXR5NSd8F* zjm6lojqXov?WKf0k&3VZi%cU45hPQZKG<0$689L0x^UvUL3yR;zJx@yxR+gFMp63f ztv;CDDYAyAO%d+zT!KS>EYo90)6;j0D?PryAKl^hiyHc^@<7Q+znAcZzzyGHl4y!#Eb1i>XgQ#L%x^tv)Q%3kF!Zji8<()Mz_Dy!^xobL3HVARDq zw&$4y$iojhx<0$xxMKxRjq`!~eYxf*7(u2g5|FTr186jj^h%~|R1)j~Jv32N>b73P z1}ey4pypMl5;XVw>0s^V=y|HClExwMTn{zKpaVXJ`|?re~3A9WtyEnVnMZqrSUz20OKyRy9J|BJ@PKyRw5;J4?? zf%(%HR0H*lM;a9b+vR}tkP9~qgE#qjjwhe%!LdG%`XH@*c45rl=_p;&I+BoyGB}Wz zY~e`5p)U+NG-ZumL#7BGP+;=~3YoRfLb-ct z7uV|-(SvyzH`D9KmFkiSp*I+{4Rq%h&lg%TAygg*Zzsg=`xNt3(-fx{%YQ4vUiAN{ zt$|oMt#J3;IcY)TIFJA;BTHlXfCWf1&{~j2Yq^Xa{nD>nTY}+QtH*qxS>8Vq4>_Yn zdP>4^j$nyXjc#fXM8lg3Ig|!xe)qd)olU=J1hBKOv+eQPbgVr$-d=r;%6+zh3gbIe zP^IQb$tpKlwqtEU6Q>>k&T4?yY0{_p@)m#k$WnD{%8zqCBm<>I*cwpgG33{+>cO%H z!o!U}@iD0O_vy^fbs_mT4sQxSvugj9z=KS-C8t6%tXw<;s{BzF2Am9 zxJ)U=t5aK);R>#gkVRERIO!==UJ;24`b@iHerc|59E8Mda2$-^3{#`Du?@8HRkr~X zrLx_42I=aS2;RAvaZr~LP$__v7D-cD`>?$*;d`KWSNV!>9*4zr3D}GU%iP1c%N!(_S5)Tkv}>IRksPK#YC(!U+=hw|k@S067__w!{9oHxRLo5ysr# z=7HCJv#Xw4>u_-YnVI=NBQpo5@kwqcHhWUmICaBgV;}DeP`s}36$W+~aF96kLygfV zFqH_R^9qav9mWNq!*xl7w$r{5LBqyk{B`HCFE%<=Pe3itGB<1c*Ek=Qd2A1FIHW)< z{>dTt(C2OY`+j!hp&4|}p?&~#Y6H=hl^*hMaO?J6#U5xUEzL$p&wk@*Z)nZB`~O2& z1AH~s{Hw6$M0X>1YUQ$dX~hqg1&XD9hhrRSFpvlw=j&cF{;jHusr=pS=2XLfOswjI+NhneEB;(KPf z^r$v{?YMitnVG(}E5XE4@*?l*s=#sBZDY_0gfgs;;#WE`OQ+eWbRzH~@8TX8c#MO* z2_U(E;AM?Tiq_@R#QRl4+D<5bC@HnhSQ?=|94YgvhJnn)@m(!JT1%`VzKojWV$|4T zaBPoB{0dc{LV#M*f%C6r{iml&@Q1Bu1s`0B|0x6m{F{P-yK3n8>V39nipDWcz62s) z8iLcFzt?kLEc7je0t>+gZZ3Ctt1Z?Z|6)3pa2*(EFpI<=5|@=k(H&A1TB*HGnS<_y z^XkFAq07nPTv@!#{Enc@j~r;*hv=KxHgH>Z;_J$UX_GB!qke zx6sl4>?rI98YP5e`0sgU?3;4Y&`5wcx$f^vAc$l$_yWbiXgS6cU|o51E+1P~S80Yw zHu_ag(w83$sqtypwwuBm%cC``IGPXM^ydN7?IZf!wwtk)VD!q~h z`OX25A-SbMQF8!7ATWi75c7zGUmf1h>;k4d5Q6A-4Ajgn&5U^c!Y(WKhjT z!}HO>)U)i-FAp>Wn0B$(mqhpBEXgkm$faD?E2~>hcZ<{HVrd+;Y-9T>_|f5c$#S4! z3Ie!O3z+n>hirAXo46^!M~II^qA9tK&2~DIr%bQcCsHP3C-oTGTsZC@UQQZ!Gkp@J7Kbq zQ}|OvA#J!KDo}*$u-hPN)Bru(CA$YNu3nNhNcrL#%#wHUiE4O{n4srvy1^tt7(%>y zreF<0=LZLRls~Pv5aHmVRN%lwELLwF)h-k;IAs$a>GQA=K`=RNt+ef)-V6CZ1vBbE zUH7Fd;m6j3IIx`Z^TFPv_FjAi9qd5nP39EI)MAKX9j zIqQo0IF|hi`PyTw4`=Ma9W5hIl*m5^IVc~Tqhq^it+WsQk2D>uM<4t3Q=!_`g~v-2 zk(j_5g;O81+)bJjTxOyWjZLP;h#tDQwD1wV>`f?~OQx~KzWGRwIeBKf?2RaiDY#6R zK$>6-F4HBUCzTT$ay&?K?fSu>gVS`*Xk%7D?zo&u61&0_>+U*8ko6*%Y}uO+)@uXx zGDXby(c!b^F;6V`<5ImVc$m)JT*5)U?A^VY54(x-L1ot z>B2bjjD;RuXkg9~&$b#LYS@GQ?8SE=5tI{tTpKtDch>$x26fU!KS7{An9pZ7!?M3jDgeG`f%A;tP| zS{Kz_!Wv!?BYs%|{+_T0-sVX|%>v6O^TanbH{HPbC*rY_j3ITSLv~lwtTc ziPgN6W_&yb>;~+6n#XRphZiu|=IEH%+uk-*LbiSth6}TBnL|Nk9I#ui%_Aj5k zy?mXUZhe3)i2qYWW?}lU`k`;G@cRDn4qrsWatU6iWd*XWxyDjaOEs9ILaBKwhvuzd z&N94YUH`6s&u${!07WXhTDIE$Vu2~!%O_kci%dB9*UK&thKacDxpf{qd-S2)-=OG# zEU!#gpN@7-V;-^>f;_7b$;L&ni9Fiuz!Vvs8y8fw3?MUlz2c^HTN|e>*0++3zwx^_ zm`07ts8^AWbBJZLMN_N5-~`5iKSTC7tip3QFanyy;Nb36V3Qi%23VvsB~~P_6E@~SY(s!Q+aRS=I{{xeSAf+)He&nV_u}6lOtt3xLpRQM!dqJavr6AT)6( zUvs<*a79*!`9pmedp)%9_8vq4pOk@jT;;kOkM@fzaZrYPgsyrqbqzQ&Ybe@)HJL-o z?DF0{wnJ4ED$r{?%zXDxK!B-UB?u>($&uHctj*^g?~2auC%C<_%9y7~*S48eUV&j4 z)L||Mee>%U7;r*PDJ@W_E&YAaQnCIWnoo}d)I{c1jE$>ZW~omw;yt^iC(0#?rjl$ic@%6OXJ`*2S4HCxsxvytV# z(Tbxm-JD~Y$?Vy)w{Kew=tUWCuxw0u_h|Exzh04gclhF$bW=5$nX%PrWch>4&?#wJ zN`7cNY5I`nrZ$)UWH#%g)R?JpZ%Txw#1iV38%IT_KXy)1=x$1OPdrpo+A8uz%-0*J z;3tjZq&A+)m~rBh!4mm#D#5H%?WgjHP(`Q3b;ibEb!j{0R!ow23>PssZeeI zdO_+cy(65YXZwQiJj=W!BdevHl?427fiT+C=ooP2rx+8Y@mT&S3ElY9NvKm|gYv7H{puXk1r1C-RR>ca-*X{O+ zw5Otu0NQf5m+v+Nr^_3WM8wrZB-JlzJ)JR=U+88(a}SX!sKLo6-Czb;-UHji;@B%T zrOnJ{spid?lTByd&zf&tvI`+Zrwk3s0<}86>LbOdBiVMjn3y)`pAnaK9U64`cct!* z>JRu4*qzuB+gCoW-nTVA%hu;tTkn)NYFjLEwEX~Yei|g( zS$9o;@n$!!(SFuu-U_*13I+DyZWS!C)wAkZ_pN8D09SfK0bwz)J}K3b3o&W1{R~eg zFjY%q9edi9x4%!{(Z1vxbR#ePw1g_|DAgmZ4uN1p930E(mLEjwB)eW9ipGuCz}PP0 z)I)l@PQ!0U_Xllc1f4*hF+jG*54r(7Od(mJ*G-#u4cZ~vBOzF`&w-FZ|?0Q5AC+aTIG0&ro^ICPxZ^dR&k{6Ui*;6nkdkbe#Z{}_B|L9H%?UV3*Fga>{V zA08`2HAT0adHWlJc8+Bh=>{HM=NesuwmfAE#jkQJs)H77>xC*n@Lf!-xM^`oj9Rrx zt1pMleU$@pHWJw`7y-EyRd(RNV3H8Bz5ZIlnS=L`qBRvwW64fnB*))V!s;hPS@m%Q zlLUc{mU*Mo`VU3m=f7M9e*UFSS<}A4?0OLT_tvO`NZm+Zh;(6COP#o09|DA)^3Fb! z$NQARgN&ea$g@BODUBW^#1Q!fA{$))1;KJ45$=+k=`Ks@H}2Qlck5m28Wi&i<|Vk4wi@%>;~XR#pvz~Lc8JA3*y z(X|eY@b7URAz8V?%z1q5W|P)SEy`FfhMe&@jHus`c2Qw;PL_2m&Sa=TmI( z4EgNqsn!zu0N>1pwcvw@e8As8f<*`4!t(boQ9P5)OcsGOHvgIp2FJ;% zd&c+Jmrcw)W^B~gH2uB&vvnHFWzqFg2J;o|hUjJLuPC;;SU})Gwcro--*#EZzZq8D ztIOoE><~9kd0{{8)-rSIYF0BZielmO9&l2L$w%Y)XdYGz;IVhtPD40swZR(&v7$ke z-CW!JI0N>2+4e#7znKqLNJx=|=}!oAQ=1g> z5=J0~^K>{MocRmN*f&MjvyOoLB2d?joVQBHS6w84!M7>vLBB6pTQFxuU4O2!yd+Th zI*e`mUn5QfV;x4Q{Q^8t)=gV?4ch#HZM%y=hgx9*&LG^Y4tCu?SKQ_CGR%4kOncr* z-e03E!@Q3LqFR0(=A;#To@iWu+^a*U)b>0T(4S#_5y+xj6^Kig6JnEB)}uu8>CLWYY-vR8@Bl@JnLFvY}fb{`Gei! z`AS{g=uRe*9s^ICn$P72jV;<1-P$(JMU+e_ErG0}PXn;dFfK486^ZL#)a=(5Qjbp5 zQG#0@8&zMb3V;jqmgEMA$;}+y-)S$Q(Tlo<4RZR0AaX>kt^LlRH?@nSkmqH7Laifw zZ@8#a#Jx?KDu(aey3)9kOQTqz&Qf&Kh>iW~_ly7W+vl#C_xJrS*ijSZ*z z$?5YbRzS}8I_h%5%BP>lWQ{+qWaNaO78+bST33Wlx%kV&1net;SUyYXtQ?xOo^hL% zNzWwNWBdv2^9V_Onxuwq zqBSVyi6CB~53T6H4BJrBdw2fqw#<#Wehl*$rKUcI9ps=eo_>dSkj@kHFLc19my4UV zmxPL;JWh*-G5$|fiN^;cN9QWe!tFL~9oAM=nOAM_#_x_8W_ekRQaIPh&PdXZl~=vV zs&y4k59TfFfr7BncPB?$wkPG(_*?^=bETbtB}6HL*Ssv*1d-6b4@Ta`f@@2NCu4_Op5=@0 z1gwz>TG!fuc6#n!N_-kFp$~9L6%S!xZBDe_m!XoFD*&?%%;j2Aa&0ta@#QfZVonJL zH%)ng9^MXxqFa`Z8UG8o`Y39J94B>}_I$ogrJtKy_uzTN-q=iA+2eR$Uqw^!l|T`FYRP*Jn*o0j?FzVVN@| z!>>GPl{}`(3`j*c)=aI#Os!}X3cKeTEjUA2NbNlT^lRrurH@%?Y#0kZ zcW;1`X@?LK@@x^|0>%{M1^wBa?2}3vbY|4OG?8g}4DQPj^?9dG@-^`d%H0#7%L-Dv z$fd2~7A*snijf-K)N%~=ZD1RviBKMv-5V1D`Y2^=891o*3StMdpGu7YMHEV{2u@Qp zQEVyCx8o5AN}3glgV5OrD71}kAp9SV*gmnBHme@ik}rvRg{=wILk)i8l^ z;n=l^>vv8$*HB7eisGOSdqK8UCUS^S(TRKYM2U4 zwQ@aqbQ@L`Il@{J@K~6XR=5O0`Kr@mTIN&m($KV{!=Qjri#QqMq+Fl!Aqc4zY{Tc5FEzfVL3J+*xw{*;)>%j+}ekX{~ z=G?}ZdWmEj^jFPzw?#A)dsr2|^!9$qza)~~O(@v{dw}VeeDIgNH~!hlgEF!3-^8(; z4MG)@N%`@aXW^94XQ9_vg862r+(JvKpdg$Sr|R8?>B(+pL$exqx8>uX-hQOCN+Z7> zCImMcsj;)pT%{Bf90f%bnVYA9;-!5(W14M9I9B#iJy1 zhICSl$0`uA9oEtbC^96Lh6p=PV=$Qv%uA9K6g~GYB_KVV*n)FidWBz&BL!xmnavQh z#nlGO;?opkFR`yp_-VWN26UZ^wSJ}>=qkGguxC?Lg$rPbQwEYvz;`=C=Ah5eXA?70 zjz~K^WLl+;qcd=P0|Fc;79M+5#h?}IL=nwA);RgHoI;z5R(K9`PFWm?AmePb$p8KE?Njb5tV1RG^U6)gP*S$`y z_rp3%L!ysFi#XE&nL{m5_erL0{Co~1PfU5E6boE64thlj4EAAa z$ipmUT4*=FR;z3-JAwh6W9WByab*;KoKQ;HNug{hZLW~o^Qg>Y^uzeH_ye;}v(!df zu@}%&r*u>F=9~BwymI++0dE^VhNtm~MR@3r`WPCF)B=k?%o@#vSOn&y(u(XrGmu+c zUN62~npn;2`jA!Lex|9Q*19W~l~R(%CWrI;jt~=v%AgLqD-7{W69vcH`?Az#a|k>t zjAx=#zXrgkS!^KETok?V&qGTZ&bYmK6*6`BXsVWCp_%6yc| zBDgAo=`muyw^L!MS;^)7Qo4bO%yi2OBDQuUM)2riGUlmw(pk5^vR1 zaHe!EpdZaSpf+#a|Gr63CZ!qvxu7_O7c+T?h=B;;qqvSm!A9#T{M4&ie}J4#6OGAs&5Jd?fnRCIz$a7;R__LzCb z+yY4W0o6`Ye3tpArVOWsvrEjL02Y_Af~cBf*$&et&Kwga@_hl6Q70Kqzp)#u0MA6x z*raPc?|ZE6(vdWLc;{yg1NHuNXFRYR?wxvkF#h4n7LsmgcPf%Y+FnIiL=DZy)(qb9 zaEC;(5$skQH|O*lX-a^}xhWGDAZZ}Pm;*TemjA#Al7n4I6PkgBY}Z3?Ojdf=Yy}-W z3}Vzq$I9kn5v!BSRG79mmr`n>B2@zGRH-h!XOV_AhD9{pOuGA2CPZ=8CdLQ zA0UJmFkwzaayxo)9A}lwLfFCC$J&8St^pwmX?l1j09(#pOSr^*@D*4a6V1u;d9tZ6 zcl7Z91XH`*=;lRlcdtD*?{oZBl@gd9m~~Z$6CS~vy>JH1P_sk+B^hVCuJ1cWP-U~#IcgZ>8xm)6;K(A74^^6=>wyAA4RtYB*laYg;w6fl#hg zmL7rl4GHgDw6pAxQEtwIadq^D3k{3IESe3_1mM>?vih8H%0QyjvjA(l|)=c0*lg9lHho9+*jUG3vc2H9GZd}+@ms_W<&{|az@kzC{ za`3+UxFlx9q~@(MK8t!hdhkfva%*3&0ZmP+DnnFuWB$GY)$bKTrqQ3 zSpE4lV`XRe6}J50VOqA7(bt_d1J2y&*2n(A;9)`G9_KeT*jYWT=?W}5uVAdiW5RXj z<@hwDMrDqMhQ`>@SMqcz8MNyA@Mz1t`mRMeW>(n9!|LmDHk@l*L^^Sge(EGOG19w^ z;ytaEl7;eD+gzi))LwP{2x#p5HAXk(n{|%cOnE`?>TfuzISKFR z>Cj8{~c4y97@te|gfZn+~lkH3z-K@FBq*mv7 zIYq^9A$>EOl$v5SP+Y1nK_TI<9~7G9xwH&FabJus#h#=z)ZYB%=#^y*KUj2A{l zgtL_8h#o|a@Z?`y zxS%ea6H>wmQLlrUXkC5`7p9iuwx}9$Rd@U({j}N?YR^B+3W|S-*oM0%}k|5kV1M5)b9v zZ=VisTue{c+2c6a{Dx9}hFsS?ab)T{0q}C`c3L?y<+eHT29BJh0*3V7&~IGC=5J`) zxc_NEdWISMY=NzwW8gi$pqOfZV`^xPfVhj3iXJHhY;~d#a(S7+WO6rB>LjF8h>#B6 z-`kLWtIBC@Nti&>U{T00K$z!{;mS(P&HcgQ;OOopv4bW;tq}}#%Mn#+8H}@XG6Rwm ztxN@X;@tTU3#qR3@%)s@Ecvh99_|=p8B&t%-k2)*Ze**zb<#2so?b{!do9#^?;9TA z$naj^cO$E+UBB`W>JJAqs!J z&qDrsJ_VqOu+z%S#<5?Qv+uTBKH|Ri9vv}H1q#gbuM(HNp_if(tZ}N4V)ROhCj$OH z=C{$EXP4`Op7EzP5msuCI5O0`lM1Lvu}QT>jLmq)2r)AXQ7#Q9KmNc7%*}X|6Q4*; zpmU?J;`1tR74HpdBy~b`P9b_2GSMz^Si)Ln3E|P6w>K?_h!@#P{culH3AH}+vWaNE z`811rk{>a6q(u&uVaDB(XcdZR0shN@`%Cg?rEpGv{P6O9v(8DjUX8wY#;5! z>hwzWPM6IKE*qYO;XU7cQqP6QnSak0paGktlW8N0RDW%vD=IR#Z9oC`8FFk-TE{0U zaQ89I9r>$Mhj)}Z2<^VhosA9rr(Ewgus0`0585eH1hcyls5Hs3om$AD$rc<2H3t+U zbDWSSHR2FKfS?#y=stsH-wX4*W0i^%eUYs-4vM9Kp4CZyGFT|$ad%kbw7ofCPdLFf z*nAfefZWAG4Zuf6e|^Nz4-c0c$L3LP>hqv{CR6;Ko|Qzzh;P5vMGVEIm)|6*(>X+I z*<`uc5rB&a&QyK&j7osyJ6O{+&g5;(!5NGQpI6qF_on$L$r3JubKF6&_6CIM7NAFD zQN=vIpYWmUS<+hy!qjgbh&c3BHS@=wb|xfk^8PmkDAPWnr%(>nx_T$aYZ6S79ZQ$Q zSab{q@+t;49j+BzY5EWhf>wjw-Pxc>G*W+~$HKJJ^1bba0ofPw#bEiq!^043Mu8R? zIn;ri(fCwxrsfu3M?XtTm-ah)Nv$E9$R0$qbUCa@wt6%uEdE_sk~8buE0C0&9;=STj^&2ntpX}ynaViaPPI5~+86D+CuUWnE z+JvxfS|`2Xjv$G>1g+Er7?6j97Gt*-CV5wH?lSY^R39+#;f;!25uRQXL5PAwzidZ;mTYc!kHnArH!$_3wxCh9y{ z&0y4OqY-gz6u0DVfDXogFG1vjm2~bzk}p2Sm6<4Rz?EkdqTRBL&qyX;c-2Q5PaM5S zhf}a$fC~EcK6kbrrCruJbvVi1op0{%!=!WkL>Di1$>)zh)wfQYqpr zCci*O15)uBnK#zOHXTib9rHNyIMzyS%$;ipNwc0iO)KC20d8}Rg|bugqVHl1;MI!s zw2EgF=_J6wt=|1IxHJ&G!Gi>fL!i7PNb=V3vIgh^ zL8hivF(Y96%{tbCfmOXsC*G5XD9C~YIO71F!Z-*u)Gp%xbBA3dHsVClU>)zT6Ys)3 z$Ykih2sXIiV6Nls^Qy8984yt?5DrDVUuPO~Z1RY0;)RBA^iQ%=Kn^7)82iLT0fb(Eqty9SJb)&}hTuqrF{ z>>)t!adDoA$i10}V3DHXb`98QDCC`8fyy#cY$}1WZWlGqXSNa5r$o%U)pqVv0x^?^ z8;a!CENdc)*0JVDUBw*V?t2Y>EgIvIA(Po~km13%foEm2n>%he?>phpVK>tlBMnK| z-{8>2DxmTTr;ZdE=rkiU(i$=4fXW%G%c4fnr;NJmF{wGmHA!f9n<=oknmKQdfIHMT zVejQxRPL8L@p(FnxyJMeYNOv*3qUlU`x}VZo-!(Yn|{7APSOLE-Yp&}$~oc$Wf)fr zP)?@6S#|E4bBZo)Le@2hUz*)ZS-rt)exQjG4^)a5!}D5EXaW~j;h=*!*6ebW3iiM$ zEcZD0JY);SDlm)dH9NM^P;HhC{N$WLT-%c6@+{z3UIt2nDTv?2u73Nn}`uO9sH_Kz244fo8CTv%66xcnzYEEv=+I5>4Y zU3TT{H}OYo5^#`{ArrJjvDBSl-dkjJKJHkd_iV^*s-qH`6r(|KJO_*e=ijVsHR+S6 z7gWkY;D@s=Okr$}?e)B|8hnGE{BE3P@q0bNH{+|xKT}}2r-FH3mx}!E6V!u}3=_EJVpF6z4 zqZ{8jiQ-Lxir{GXJvb>L1Hy-!&Zf@wl$$CC1NLqB$?*6eE(+aUZ%_87FVWlpF0d5o zezDisBas&;Bh@PCBazr6s^y7MeKQzY^fdsA#qFcMg>?`2gy`0fx^T!)$Vm$w9LQT? zZEz!d+j|pDtmrN~*56Vvx1UA>$;oXm>Gu)UM8{J$=Y*aHIdqaQd1iV|qoP_csq7c< zdW=fH+PTD!NVNED;NC80E+d_Zeotut`|Fgb>KEiO@4JnXv#$J2M3iTyK>v%hyNrtC z+xtY{I0OjpPH+fLa1HJ*!Ce#F35~lu1b5d2cZVjp26uP&+vI=Fd7im5bMLG*i&wC^ zimu+(RlD~7?C(!f3X-1QxhkSZeQ_-LDE;dTmNwB^^)M!7lhU&Z9318cm>SKM>aq|C z8pi7C-7TNwOesh*Af&m~Lm{=(&WJ#iToS}6V06p5s`C0bLNk{Xtod?Tm$^^ffzgIJ zIC}2u7scPs$_B?}R|c0Kv{H@meydqLAG7t8GSm@kN+faHG4#`fUL;TvMWRwBP$47v z(ih_z-l4PFrhbH7`R$6p49g7u3Ys$4d(}HX@9yO^5H2r4BnwDG;sVKPu76k22j443 z^l>rvAlwT$RbEkvvo(|L#x|kmytgx5phwgPQ~|2I2Fl9nif1TSy`MyGbvw(3e=?S1 zlG;%+(5`qWS>c4HER}4|q&xc0?%7@DsYh&4wKM+Qwple!ubTDjaQB$AM$Sw+!Rrl3 z#KlPJ6=hsDh(`Fp{tQHMV(ih)6m6f~aL{N5^a1+%q5OnvIy`v=`;=}GzommqD0`dX zS#ehwVLDE=$abn@hb5*NYw`tttkySRC@M88z;uw$j%e9eSHx!HQz4}5FqnbsQkpkuVPS&!Yjfk_I*(0 zZqnQJdIj!Xl0ZZrW>YoLS5B8vxwg-?G^M~u3wfsy^l#8aJF(-Fk)E!qMzr)E{p`cs z><@iz?P`I++#v|mJ$-E?%LqSSo%jxosg4)8FA|(VGeQ>A7*Xhqz@*5DPyE~QJ!p!c z;O}sxzxu;FsWDsTJR5d4Way*{0R`R!F7ePKpj~al=TPH_9Nr9CW*)a)qAO%r009a- zY$xN`!m`8e<@RlGI0u0FY<_HoKrkSU-RcQvmuG31x`o_Ukrh!v>r2&nz_Y3OJrKh-12N3 zX!HI-(a=&DE6Fa7O(6?6hC^WONwJV+u1`3%`jHL*5>Sl*_woBLF{AM7 zopWm9@=-KD=}}}tlYAXJG6cL;xqJZ{E;yX-_J7NJbao7fNQ&fX_hl^{smUDH%-r)3 zs+LY*`Bi3Xc5L4i!9Y3>BDdH7(7IGm^Pfkh51v1$pZ3CRZM^hgUb-by^9+Uj%GFJN~70r37p zp3j7b$t&`ZBnEKcXCWMdXayY5F0ek6XRgzLGGHHPB9Ljq<~wb2yyT)+X<-z46!|wB z^5TUVTe==fYhy&4t|I6=)A}_T8cZ$D$?z()lhKAxuJnm8YO{iX{<~ln6z$d9xv{}* ztJ7p%1xR|}uqmAk27dn_On@(Vtq|l!N>LjL!Ijbdx3jCS074ZJn?~bd;;EChxY~jI zui?@Q;knSM=KPzff@O-xNfb-NNkt@9p)T<*XfBFK8WgykwZ!3nmWd zMxp3c@YNkdt7LsnDf8}70VPt{s`e2PuDa8}fDsUmIy7^Na_JPIdR#*VP#h59E zj!W5lc5b|Um)67JS)!4S=J}6lKGi$|J}#_q=W?#vCZEpv_R$%$l$9yKS{wOLiqFv8W;-3!M@bp8e3OG z>FR+^vtiVAd2WIP&nk8_}OLXD)eiJgV0Yz89#JoB^ z8$o;2P@g6*V@6-?ql?12B`(cJ$PqP>CJ2MdhC0}t$#hcYxsFJ@5y~ywT%;XoW*F*> zn$^&%IF0vveiVE@mEf*d_92tm9Ir+7Q{%D?z)4M=z}LEg*8nH^(?=TVFvXzX5V!X| z`QihY1YMHEtAxz+B9H}|-45cwQpb1v+7U?D-iJnzUoQb&Z@0ecmP?Wj9u3#At$~G4 zq?)GAsN*A(ICPSa!#{*@$pR1)I{U#p>bA&yt9gm&Ta8&_608LqKpCrSfJtie&QM1k z%io`QOZ}P<+%=ZfyB#JyIx^W_dzzcU@$C=-VJm3Y?}^r0IWrHy1D(5>)x+raP;FX^ zR^fxf#j69wN{VJ_T@4*ogFuYTJnU^dv9;_uvCBb-2^@ux0WV2rU6X?OiZKM|(O^ zMV;U=l>7^1?VZ*oQ2P+RY<_NgUU_s24RLZV@Tdk!@3Txo9on}rZfOc|D4myc+A=zlO&FTx|tqTx{r6= zIwyS)sDZ?ELb~8?v7$zcly@Z|-_kdiKOiC9nd|x=i7lm;HjRq-emUdN$i+W7PNAEDY+43h%y992`;nMK3RQ_ilW3~6WeDz8Oni9^ za`v)j*>smlsZPxAuG4%5!DkOyZA&I3z8cA39TFA75xqxwdp?1dWS_<02hk1L zeUVX}E9^9~c*XYmz2)ZsNivE?N1d`%c$`8&7)(6&(qKH%$9I0n^#J%HdQR@l;|IC9kMbd#%V+p;6;tV^-@lzI(|dURkzB0`Z)a~ad)R#L zdJ>F+N$U6s)bZ9gw$rbVOr~_~Lo52m*}vt+$O)S}0eyr_W`7){&ekuyoHc&P@R-CW1f9O(?TA2YWHHaBMr3UT>&bk7-gz_I zy$2l58DWc`h1|Q1>2TsFNX`qVS~4G%Rnpqsr+0xX*X7L_N&DY^*Zst;SzvMy6l=cn zCTL6F>A2>eX${paid!+88@T;F`>fwr3AiG znDLme_W1BjZ-PK0FXZ;&q%-|IU@#ElBKObgqcu^XZhG2?cgxp~BA#QmOIG=yBAlMa zF>7?7YSp$$^mz+w+)VEf?Ez!Vi6TkXB*~=z5>nlE8r7t2VR*m^H|vON5CL6V@HNhH zt!2|W0gtmut-VLpxg@hJ`m?8IOD)Dn-ovhb@^$v-u!*Xrfs=hZ!~F<_^y~Ihiw=4D z7S{(#8&`bufk@A(-xp}e$(cYS3gLY=TIFyc$L>>e+vvU~mVMTiTT{Y??d+pEWjo4st zjn6`08&>vHEOW5Y(z#}ca?s6>w#TF`m4}pl%M{(0C7wrY+po*SrpzKj>I1vr9bN%5oAa*mfN0JD!_Z z=pOSF6+3D9C&d$`m$`47q|Y(=U5WQGa~xaUG7<1ED0;UxwFuxXuiHWs$=pXzZ`Jb_ zMGAYwMsv*BHd}DI9@k&X6Mr0BdAT?Nz6{TCN37lD57+8@{Kzou@G$6z&>%XRYjFxr zgcEaIWTIRvRD`N;Z))#oe{y^td)P^9P#^K_e(P$-tX%l$pZ0jkRhf^wAT}a4f(CV~ z7_b4!_YVGjkdq!b`P(f+tirROjqN;A^#(z+-Y8GM({gq18-j0@Q-#y)j9p> z4t@preVs#I_^o;9cOh=5eEoZTII7jRz{D0(mchR}auwcwDAk77{UwjjiOj6Fo>n?U ziS&ng2EX*+Yl`f5Up*uYuh-NnX52zfPXJ5ToZe99Rg8RxYer>HWSlF(XAz^u{$Uqg z|9XR8LiB@DBfjc*R7h?huZ6Avh=52~A+y{DRM?@=`Zsq?YQw(PwnbfO3#>e?VtUni zt!r=EsUZ3h*h{J~;d0y+1rZ9~qBOUG0v1p3?-r6a><0D-;iSRafy|PP^|~(1;2wM) zL^wzr8XkmT7}I3Ya%9j2=oW_}b)V3O_kR*we52do00IDQ#EJ;6Xw4r+UQMvno&ZRi z=!C%Vwp*H{(%%8i*!4GEtt!0~OzwX*wKg=g*YBVEC)PQW`ud#Pk+5%y!O)x$y(9h7 z>ND#LEz;NJc5m8hl`|xi?grMz!W(}-^v=U)(U8Y)tq%S;qCty)#Xb`};w@GyDb>=y z+DNFeVGU+t4gB&qMK96?ZsHIqa2o;N7(FWV$wxO~=*GW|bmn(v3vMJo+J72p?vb{h z*nx%a2Mp8$tG-*cfsnppV%x^2PbiUH`=%a5Oxn-WH2fdWfek#6kce~MT1+%~W=FQ|G#AOcd~bT**o8~~`H`#SH0z{5GP|D6X=hE+Ke#|WgHr>$0by@BtS-+nTP z=%r>@^Jmd1Gi|FDUSz&AD6c>R-FI%)s*ZY+ zsE>GC`qvx(3aa_C$bu1W`iJE%pIzU*+PKqr3IaSp5ds5>8F(%C(BenWcr_D&$bUj+@Ouxw_FllbMuH`}T-w{Ql7f#_v6l{}#z`6n z=eOSF5iViaIsxOh73|jvK$DeqrR-nL?eG_@g3K*aBfUZV^#Kp;*Bn5dm36Lc%Jr-$ zgj(vG!S@0GR82DsX1-rASzvr?3g)tUaT{0;MCk>e|LhG{5HA2bKEV!Ep3Pg&8H}JEfm0~ z#mJgM?T+G8B=TFOVx4x8M|dy$_@9>@LfiXMa>tWm^|*+w2GU;e^LA33D^uheuM}q8 zpDq^gzchL^K4c_5dnxt~dkQSsf~%ijZ#l>$J9v@&WVfx_2j)x$j@z(3jdx7#FD)Zg zQK1t)3ogI!Nn}GJUP0}-4+*iXf04NV@s!-~`ZBhd^~$w}gnt}2iY?)1qM)D`9i8^L zZn}7^tKztvzbdw0!FfohQY!Brq&CX#3Q&kA8ul-0b>V0$(k}Y_a(?%ftKqxYk6*v4 zl0<^i<|;>*-UH{*vFCl-?7qCuzpXQYcS0gtdqQ$>_{`=} zLY(z!67FX;)(B8Rxt}#EWwcI^#w}BPKBYX&!!RM7Yr)oBYg^#$+_+cPE z-s`*f@X2n)!1PAuPuPL|mQl;zJNfT>zb0M%%m7^o(bcTO+k>1LE58=YTQby}ndN~k zQ4yep^Svflg4oQt>+|oI>_mG4onOZ=tsW}mJ%ZVr)1G6q88(ybekE{);z5ph!KI|4 zPd&N0t9!P}rm}vQ{&76|dvYyjf$&Pds2&F<%~lW&5daf^ozX7jRS@uGd*ybbk@Nk0 z881GYA}I=s!X(J!^iDWs-up8wWQrD0F!@ zQOcYt_18>yS7T6y3Q6f@)RAt|+9kZ0--9!S%W>RiIVFd34q34SC#W|t6Gp*ME}@`l zH4g8^&71^H>J-`V-U05e9LDR9?ckqkYUPQKpARtk1U<+Tiuu%`zq9(KO2>=eiQ4Cn z_Kal4mI8Ina;OK&MK5|&NLA6?3Mq)u$g~UrV_?Ymo{vS;HjP3-nb0YBo|r)8!?zS+ zgS6ESMxf=G6+gFJuBhZ3dUc)4yp&r%gmrfY`{+OhH1#~e?|4LF!&^!D5tf<8uE?b! z=3$h~kb}-vt_x8JbwT-#mNJh zd2RgP#PmO4n&Q8MX@@s3EiycDC&ODrEX`iR3l_leYGe!mUfRfbLbU8qs+~@x3^vJ} zh@^dg>JZC-y+CGja`d3$Z4gmRo!%fqP1dKa{5~>szoV)We5D?w5{VU3Y6*s%wSX#K z2VhvlCWM1kSLA5Sb`*Ieq&~DGx~`eK@JLCj1C^tAT0k~rD%>`$wwP5cFfb?Ggc~4aPuRbc9bPed{BJBG-TD^GI z-K2;Bc?!|=4`bkrU@3RjIrYh-^uY80YoPLG_+PC!A^j%Oso1nLqi6L$%>|mM@j_GD!q-M!?Wo;F^0Zuc z%Z2ESC~(wMaHLt35`Q&lO4o+1H}hHDHzs6T1f`5AXFH`tRAB2dWyhn1ap)%0`d}LC zwp$pcp8An0AxE3xDRoYGtkVQ|dISGOrQf9VrULK9v}8TqpPDA26$-QuYvxrg8wgFJ zY?L)0%0n5;3x6sTT2k3Uf;2xIr%kYS1ZSYD8{Os_;@yX#hiZliuj#{8f*VIfvZx?{ zFZ3OhTz_77;;5o;PFVJ+-(*N*h=nX^Wbw&Q1f>i%(@Kf74T;nhptD|-YX+4FhS3*U zd?msXUUT*Hh!~n01N1Hm^-vd<>$nXHbTofaaJ_5EtRcj(Mo zeg#d?p((hgOMF(&Zi3&41##oeuIi9QJ4F#Sm}8kDLukgP+;!`!7PK-2-^C{`IAqxA zBz}`B_8~v*OvY#rKpoMmTju_X6ojJZ!3IQm5(6n>6+hWtK$7=|u_UptPwMzVgA`8* z1shop-GmiQA)-|Uil;^K)xov1#jWbYLuokF37uJv zrO=P+QDU8|XERTU4!P#QY6d&z22pwLmsXq?;?RvzY@_Pho51hwY${tS?Qt^?3IOgq z6qHF_8ONqwgDW{MMZgEhP6WOitg=RS5@CMpvUsi4oD;Q_w_nCvZaErFT+` z8pvOGKLpm8tzUze)HuXyEPQM`!|@DY4Kv_r#X`)L3xoDVkrD_nzaJ_)iX(>nhK{)a z!gPa77e#i9@!Gx=)HzU?f6N;jX9E@!E=7bCsx{U`m{pGM;g7jeS@fp`6mBSkw0g@* zbMUCA(RH9hw3vDW+(u4JcMF@G7Mi|cQ(O2Ea^SH+8Jb}dI-DP2sRjCD(2>wfb|YP% zoclAf{LCX1ROdc4B+n1EC=d9E@R?5{(q$wiDD3w!%>q*BR!ya|vakD=ss->5!$&Go zjENMxa1?mdI=v7Cvec@vZ|TZ<@;Nt1_XdRrf3zkOtW2@hzwnA93`@rjE_n5CAja((P}89CJn2VWT*d=e>Z#-Vi|UWDW5BeB=fnLv$nEV zIiEI9PsrV6MD3olj;E!v!PFi|e9aZAb{j+_idCkO1OtO!ffZUy=f{n}7%f^ccX>SP z6o((r!*@Je^77Tv=l6P&SqnGr@`1$M%mF!WFi%j#enoS;R2}?Y1uR?0#^$O%SfBc2F4U_YGg{ifGi*^YxR#r;hVja!*j zdUm@JO4yI&R}uV%9w^GEG~6%V{}lmt(q_XMn-0RSEBc2D))u>`&7aE*(z>qi`g$ip z;_0L2Pu}cAC$$TE_`v$eBIBhykI4NP-NtW$$p6Kpjnf?va;0(wn|G=XR%@$<0%(H- z?>Y|XqE^1hM7rG8`U*(@cxCecG#M-(J{6hMC6IaM2SRjbAGt#P&P?2GxBpS`!Zu3F z?!sdXNs@(O+{CNSb3sZ{A#*6(Geik%%Z>+0qXl8S)lN!Qz4cRMPVvDHPyrfnK{Q4WaXgMnAh zsaSeIDlm11f1}QDUH?$$PmA|I47;ZWjL$M&7jwcibv6RSmj6-C9Y{Su>iwejZKXF53-YW2rp@aV@SG)BL?Y%NlmSN541^_oVnYalZAwpC24gOThMz%06jvrdi<&Z-g0#saBHK0shho?NLf_FfOz?S2LR>`j9vSU#RhpKJ7 z4AXg-@S&#u7jvTVBrc7X&c&ki-ZU z>_8_s)$~zOayO>yRu@29*ek~p7V1-0a;XM*#^$I6p-qU7J-`6OH^pDk+;Yb$@ala<}?Hzp;42pl@p!wdZ0m+{&Z4!^Je7-TzKH0R6lG;a@#!z5*Tjviu=UpE9tk#P=(lGGO$2DXo@|IR z9_AJQ1f`N-9&Hlw7HB3f&gh{Yjh9Mdd&@U6Vjlz*Z*okY7y z2L3Hv_>_xEUVY=HYh}*XnjqN682stHQvpQH5D&15NCO56K^h;wMQtO(5JVZM(jCBZ z-PB4VB)cLZ4;Xp6eR$h}E0>Ea@TLXmT^NrJ=#4v`O0nzWJOxVmO)uR{$|JOQ9LNxGi6Ei0;Y2+#fFcd1Zi5m=5m zs8WsCC;pQESSHE)8xcF4hlx0JYbTI0=9wjamb5;H}7LQ4LS{0STuPbcr=&Y1fHKS zx4^}`18~9BxEt!;hjd-2f^&z}B^_qXe2|jnK{rrf4(&c* zAXRn#QwFZ3!k_m{ED7zcKDWy(1Pkt-K|3364`0=L@#%V7uq{Y`Iy)poi|P8;{AmY$ z+YHIFaQw-Oj}fG4LV**a8MR2uzqEubS4$_0S&jWa`pL=6|(*b0zUp&HJh+(A)nee88O!dEXG+(*aNoBr10cbAk#3}0&-MR(l z+|aplM1#NK(-43ea6)WcMq)UKP1Y@;sMCcgtLPy^rZ#iLHEk`<{;DC5w8a53b%D5gPcSNOxV836De7QK4aC&KWmvHdw0BF zxaeb42qrJM7mjWAE_b(fO+E zBF7=ZQpYRKev$g}W@C8PLMul6gj0-Az^rqW@m>YSdb{4HT z;Q6B9eo~IfwgQm)FjQL+r2uR)coDVHx_nLNow~lu&NQ3ZTc|wzxH|IjhFhDidU*y-zbDAgYC%w}3etq%CJ7kH(Cj^S;PtWd0P z){(X4j!kogzu9V24+!;t&7gs+#YZoYLkG-(^9k@QCVFp3MzX7UlM#hG_Q^DOu98faAu#55oc`!M&fXX~G zON8Cnp;KH)HF@(TM0C@|{KL*%Vvmt$K zi}B0%1L91eAawIZ-J)J2ZBPg$`$oT+Z<;PYTtx%BHkawgYVgJ0y}R(uTYG!owF;`4ik5SPknuigN{ zp!C?ZXyd&m>vA-z?$ajvB@rO09bV&`4rx3BeV)wy0Zba&8P-?>7yi}&7ydS4po;-f z!KtQafwu6O7!86FF<8|5Kr`Rwrio1@%~?!B6D7@4YWN-&8mG~nD%n15Y*T*n7M88# z{|8P7y}@aDQ5nZ73`hx2DD`}ZG;O5WEeHf*<2X`EuJ~>H^(pR6?U9CW z6Y>8;o57{I|3jNwUJoz(3_coiKA#G)lG12`*`wbi-x~QL1St&`9ZjF~Ts(tt=s9}G z(RX-hnu}k7vhT%)DwZH=q&weGU%%nj+S8Tv(o6G$V+|CcdTK7ktz^d0nvYRFDK?0} z1?wEX4+i@toiJJ36&$eIotoCZkC4G#7i|m_^kMU11Fz-tuWQM8-j=8d9Mx_`v{ToW zeJZyJ%9ledZIXXi{ymY{#+!m(g(0u$T}8)tGtETPSp{O3_w66guQT!r4rh)}+>0i( z_?Kv*k+{<^yVvwI=IbC$YEv|TO!0vwO5k@GCI1Cd-efz!hHZw_B4eo#`7ujm? zC`W6kdHD--^hhAQvkyH;PwNiS4aRMoSC3NR)?APz0M{#L&POBBQdg5!XiZvEx%E1C znh~5qpfXU#y$f40vA!b#tiVN(qYNH7KsgzW@cOm#_7O} z4_r~}zgU{IVEi_rThfa>7Eeb=;DNcytJgTaP8=)R9YOR)ek<>c?Hx-^7FV%^k72vv zd>_%lk`3nx$teCU*gKa>tbc;tBF|-+Nc7Z*v8gRvH8A5m3-nol3|FOe5VtUkrSp~E zvmwM~OS_{4y6xgP_u!1j3jn~+xU4O}vGx^q{N20LTL}Q(c>I+kG(Rnd_CHZUG!mw; z#AtDKxN0J}G!j%aC>_n*epGA#9Wb>5%Hks6g!~ww5&i z*-0U9kZoCy;_#~qEcv_-(j5E}txH6Xrp|#1R1Bv28kWc)pn%)gMjlU)>%(&eVbCY2 z*<`Nqqa|xUn~AXjLS2Yx68!x<7%vY5{98Kn@VrT9YBR0B(pkAl_J1Rt@BO}^aB5pG z@J3A4hQuyDE`_5_AD`DOStFfO~6BxSQS3RQhw^tmVK5?pC2P= z1s4==SDOF+`gjT&TkY^#Xx@hkR1-IB9yg?Z=Lk>1m^UWfjW``ytdl<>_#UJ(+VVeZ z=jmcUSHe6|7p?_*w*i*^am7ANT%y*?_g|JFeBS!CK!eDh9gJt^2jj1YE*JpW9+9=0 zvWfvl1xck%fxybT(SK%`az1tXSwXE)mab>tatKloSp!lOA zOA^p<6n9MJ!a(zLeE;~usNKLN#T^%m8BR3>2mosBsjY}y`Ms`N)#BVN1^|g~{}RSi z+0j%mWl+$;CDo)HPWdB3S!Y$K13*SSGL4KBe6pW*dJbwz3n$~LrOkjEtIm5U^nsh*d1`3 z-anLF7oNC$gO%_Uko!J}dlsYn0(u83K#e>=EXw;2$KyH8#otb_i z6MG8c`d=E#T5ZHkmS8iRec2nDqiR11kk3Y3aU!sLyUh|ho~N#`!@3^JxLgo$6$Z@P z4I}{h3zxenO{1k2u@h@?#1L4=Jc1J1CLo@{<;#OpYjILQW|awT;8|$+uPIv+s`}va z<6>_%fYRro@f8+era@#NKIH>9$*&%l zxxi(ScZ}r#jjN(-jjt;dp>2yFe3!yHO)1K<-b|CPHB)d7u{(fU{-uW@LJ51dnDIPr z*(96sdy2NMNEm5c4r5Icuit5eV}#h}q(jUuVVn@fd!P?*_c$mcAf-N9)#{FwSZ zIT~}a?MPo1@nTEkE9HWApaT@R8Vq0uJd|R<_!|Kd==i*t(xoHvLN#nq39Vu`iT9Xs zN9Vrn!zkGewTBPB$28}zN_41uzc!mL7qR(N4qJV&J9dFP+L$ba5qm3Y&b=l8Oqt$u z^9C0+-vk#m*Otu2l+hfO{9DvKwyXQ!Ma{w8G5DJXi3as(d5tf;t5tw5x-x$spjlG8 zqVOM$73gkwGM#MbPXd{%sN$Dbtg!If@Bsyql)NLXqzs`6ka++@75L5CQe46SvWC*# z|12Zu;Cxhyh_0`1sgUtu7W^;Y{C~)sf&Zg#reCB6FLAPTP9yjTuUs|idMlsWx3-Qr z=^A{rUaiL#E={U6lG{ne3<4nvx}j(sVJ{#LUO<2+_e|VvPQL54dp^uO{RvK6&76Ij z<>{uoapta1U~}*FyK)9Ecl4hdN8h#y_+b=-kn+^my^p1+TmZe)xP$oMC#$D4k|tlX;1xG}*IxkNmOK!ot^Mg=8nU#xQ|>gB!{T zcZZZLwK?%Ft$63X@eTSDY8Djq4v}x@?vKUaEgcijeSqe2)p=7>b;zd3<+m~&DB`|p_RTO8T)kg z#g~CdI1y3-mE}1%o=!%EFcvTF^|!VeIqSPbaNskz^GsIzVtI9GkfqPRigM#SC{0?c z4C27*HC9WsrVHrXfTni?YFc#p1Z-RWIV6aM5r<?Re)mUfK!yFP#jjwQ{AFPJ$=QaIECOXz9tBzG(uUY) z)he61>tfZAkiMR8^s4V`JggU{f!6RP&ZVZ?>vhHkdsCy!d&mBpT%+=ZytW*R=H`*Y zpMlkfJI({uqqaE8bv~&b{2pOb)Eci-uX?nFSslbR%-gI;hfFJ5ZuK%;y3JRWcDy?T z^7Zml>*_$o+1(pPj{LW5J}lm`nbhqf&)-)AkN)>S9SX^P<`Y(k$cgU@pY)QCc%cy1 zov^3}9Glo;)-Kq#T|VK=(PEhK)r1@nBJIa}^LYp#NSNz}YRr*~vj=MZ7PvszOWtq2 zPVp{#GbyZ4Bg{DXme`ZdRGSj_IdjVTT}{+}KwQe2a=1o?zGK?#{X-wbYYiuf*8Qu> z-S+7O(xhUP_Cqaa-j7quY37y)X`AG?D&;b#4HJk~CneP()+^_gDpIy}hntT(mOOb) z=B$rmc%w!qS=6#?0mFu>C-IUK6|2&M!c_#z?nUHW&g|j~H_mQVdy!>IgPvb;S-LT; zf$EebmCxWfhbSEF?xAwy}3WCpL!0w3}?bSE8XSvN@o6qeKd;Tw$O9?13n}FYp{RaYHmHo?Zcr5-u(t4HvV7Vit_RXqp4kw=|7CF;K@y2#f}TL z&Ssr)ejkqDj5xMPMG4eHesBIl1;dukHh4#MafbNyDH!f^3dk=B7RRQquzS8-Y6YfUiLq+N{D+4I->$ATSfKg<0 zRM&!}A$PDnhIyAQ;xFm=E}KCe0SkIu%bSk_ZiwMl91W5tYUw0G2hw8WES0YgP`0KjLDs@P)*Nr?vhPyuf9BV;qAeShEgE<1!Ci)5+jA`@DXSV`KvV|BFz zAk88d;EypEW=-mYP3dDr=v!q@A0ppXRR>7(wi&--3tE6tJg&W`@d;B%pzxarP^a~w zGn|}Vg@Bkoz4r&agS1eD+|z$i2Rut<-AZE#4JiWp{vJvsOk{{Swi^l%vUr1rNeUpR zWjf0_x%5Ci@x5WB1gKnq0<)J@_v{vDAUEIT2PuTn5YwXbKuya6NZ7NA!YKxdBZz12 zWr?mWd{#o75j5`7=|D7}6P!}u&aaCt*w0G2(R-)F#~d1ko&WghtB7% zBh}aRiB*HPL(sV3rUC`J{lkrrv}0o5P?mI{)8VPl)Bf#~@C)=k8^e9wt@_JT?MthA z=~L(gH5n;HyV(WxhY-w?-$i980XN7E8{OCd&(K28u#!AKymj{{Y;+n}6G2t_iv;4821X@_vRZgbMWpP+^w}W`-vjRQ{$E=N% zdNr%J4LPrNEOvt5kbX4Gecb_DiuQ|BIO>Jci;n7TZuiz$G2q^@+U-7mLu|Lr+EX@d zx=pI1BS{Yp=!lxY+Hn@2V!hgp-L~Ene%TmN%SvUK8&ikXis?*s>54X|{>eeh^h?C; zO^4GfiHZBFHVC=TZb)G4o4(lB<3pK-8K*XIXwye45`6zD&{e;;Bkk{<*)4D8ao_$b zLsq@2gx3aLX{;Uw%LkDO1;H=)^IwYohOw@)Z1w{aKdY_FwdfuGO=4}Dz>`%WksU!l zMEe+4y9vWAMMbi$@;Nq!Cs&h8{;{&wBZbOAX%s7C{bs_6+uQSzG}nvQ)8TcVNVu-T z;Kege+HmUt?N}ghVaL)WQo}ntLa)x2h|lQ-PUb!kD4(`DB=zSDI1i_QE7I-wVjPJy z1=uf1>wY(}J@~=Ze~PeQ9->Hh)#6tEgPzRMDAdx87p0c%18sOO&NX-=)w42jYZRWPD0ub-@kCmHzQZ?&H7p1fUwv3Q~LO-taen&i(eEM19vQWQC zMU`TaeTgHntvBOX)t@n;_+p}_~Ff4zspnTrKQP;XJMm}aqOUEdpaK#Adg!i z=)Tx~f2IXQy!*Kt^UF{nKUX_wx@5?1P*foRJ#io`TS1&z#c~opizaH?J!OGS`4@=c zYD@FjOOx8GTDZTSZS?Z zE~CvYhX6~e#6*V#EaYw3LU`(sUZFyv zLJZbGAw_~z?4k(t^mD6<*qCTsIhy9Rf3ijDX$u5genrSGA_sc}higQKdJLCJZ zLV=3T-H@tE(wh_#gciA>e9Vr!NPaYVrXCvzM)=$wWL6e$)G(j=Z!oL(AOc$U|A-Co zi#Z3m6O?HhHx8rH$v546(!bc{v28WyN#;pmccqIa>+7NZE0t9og)xTPSnkV|QEe0r z8H4Ydg25;P&{5>N+nS(}r5M0&}={JfSe*i`OL^hP80TyvCwIOV? z(4VmZ>5?H_67{@BqVabomDTwR+=$!=a}pABM5`smOqhLz(A6r za%8d^lftLRO%{vQTzFjS12LJl3A!TC{9`c&Ij?!HJSK}=M(!Im4CBU%bqL_1;_l{2 z9UZcjcbZGL#CE09>O_v)x}F=mx|g$WIyE0 ziX@?n8tH!D5Hk&dPd5p~964(raC>Pm72jxmq~_Zm@;v+5;mx;_lIe*#>e2v(cQ1 zKQZH|mB8}YO`IkBEsj+<1Ev7&EsnJn&Yo@h#{kNofa1|q*0~R?{;nCxo4Q4zgJdQ< zG9SV|IPc2RvWcXX%Jyxe8KP_CgSB(0XfJ6je6b^66?MqL6VPdf$8w21tZV_n3k*TU z&dgvldCZXv>5_|~ewGt`Nt()|k!PhV+Afsoon~CJy=N6hv>lEzhhJqVqUN0)S0^*t zK~mcQ9S>B7r)Wk*r&BrH;R9kK)h$oisNqSi)yBEBh>G^31CF##F9yLrr44)|_=Af?(spG=dq3CZJNlU!-J8fP}u9W!ZVGZeC%)JH#7 zPn%EZZnb)R2~t69CCYu!&~ zBn9&KlgAhv!Li}sEQrH$qfo1lzoWRmDh^R)Hbfo*s>)>de--FdS&37Cv(qFVgmRK` zgCN;o506x54;^O}$SDp_{!+@%=V+)W%rRr1k-mFOKJGW-n@lindBAji;9 zY3_c9u*j}Emn!NTe;C3L?O6F^&L4~X#NWRg3c$R?7XEC2`@;mF{su49c_^j9`bNL% zMAsN-3CAaN`Y{7cUG>A1WUGWVagch-Tk2P5>a5V0owZ}UPoEZO2W%nyz9R}V!0pDu z)1wOiLmbNgEe=1}Yyl?zK@RoVu4;ZkjtDB1O#M-HSg#i1=>Mb*ZU5GW z8HPeU|Evw!bvK6z05nDa;D*+JbHf|B34d-*Orrc`oPg3|HL`j6vHz$I4?xyI;k|AB`BuRT_-w<(wYok5^F-_L)`Rr@BMCo~1W)+=lXoy8^X2Xw>w?5> zpEukoEs?fXp94n?Hqrym_Pq zB>BlM5;}2e8rTS)$}uD)O-S&62F8EMY4x~g+E{HD+7RL^I8=1G4k0?gsjng-b`CuwLer2hgTv=X!E*IjwlZ%T?0xB zAARpSu%YRqrw@(=07U1(8%OXQc%7OQS-KrL405usgosOVUwgQuZTJ)B%t{Aa;+mBM zB~_^nRSRP;2{0|5@J9J2c;19UJ&ag)@%5zAju-cfE%Jwt!UdW?emNNtE%urt+9Nb| zPL{d=n@#!#qXX}Ca=B^>+ox_m?l_5SX^K2vHr6N99NGQRVXY~)SNp9sXN|$Fsx7E5 zTH6iV_8Ky-^I*8#jJf9E=*#MdDlN*N;hlAE$0Ie{g$<5^7i&Zbj*!oWStzxOdG$R0 zI*v7!XX6hB{Gi=}zf}gge*C%Z6Yk>oM3Hl4&;!5a7Mkf@T-(L11ZGcl`<)xC1qk@# zWGrt6jnSL2f9U9B1??yOX2E{r{$Z3!lG@F>h#xZ<*`Xw9G@1f8D;N$mO0V2wCn}VD z!WLr=nkzaEH%3^*hGn+2brHq!GDN?Rt@%e(_5V;+1w~bVE7j~&eGxYJNn{V!3c5yq zOU=U6KN!ZzPTkvb_3TT>C>WBJas>vYweF}OoP=qrN!r|3zdtCHeXU{3%k)uJN=_C| zof$TJrL<%_Qt}B4jLrsRub{!4yzOO~5Nu54c!(i?!_g`j0v^U`8Ex)6;(D@0;-|Bb z3u?DfKPGf6V^PC~K}(wqh&#D1rdd&jQ;eU<)`*%dU^B)Jf9L6`COQ~k!`anl-i2_! z`zD&9KR1(Bb5O{D$=6lHVAWbLg9@n!uIDqN`89#Vcmi=n_rfB5)J+m1~4T zID_Uz7|fS=1o1=nzodKFdf6_W&*-~1Ui_ZVABOKO03`o4It1!(qSBrXNzpxU-h}tB zLH4RhXTdxF)lh5}esrt%cdmQ!Yv%w%VIV{434{{Qh5F=-q)i7ON*T43cg0Ukcz(o_ z!iSZbmokO}l&3Y-xfva$QXr=vK-t4kLd3|>hBOfaTnM)G!zRi;{*_{F>#*DUoXFdSFgCpG3jw9BQ_`Ad?Cv7J6i(fSjzm4k>1M-IkTG-&!ifLdBFYi)1G>y_*Z&{uGI@1pdn!alx#mZVAxd`UJ|p7NC@ZdQq_w&4Bdo()3~f!$z0ys)*$D|(F!KNUj3+(_|(JYjGj-Wax`5D|D2{nSH;3W5yui1FkGI0h)|Iw=4nF%wc1 zQKNjde6hd;>awzZ{P!+vG}PZ+R^~2HmlcbJn)Qc!(q82h7(RfpF5?^)1`_5bzOS2m zxtmy!h*)srd>t|*4eejmh!!tl4o+u{PA#Z^AcE6W(&w3gxp3zwC|J^=jSg(vXAmR6 zp<&s>vbw7)K^gX!<{fa+Oed9a&|N2H69D6fq|pa9(UlmpgbAj6|3dZ$pi;RiyvloK zGIc$rVqj!us-oht6ISKGDo)Ox;VRU>`ijz7JPKuvliC5pg1<8z5=)z~ zi9S@2!(<4hMd2Q3O6|_z$MkUvQ;B##!T%5NBH%ye zSv}J${a#(%u5aQ41fWoEH2zz@_%G;1(b{vTmbq2+geEYHf}1?h*_oOeUG6C>szl$g z!wdL3&8m%WkhmF<6Z*Ag34U##63W2{te+Y(IH)$_{`mZ4rfPbDWepmxqMBCBy3L7p z8=eB1y63OzCYfSf5F@ktZf~3eRWkYfCfL+?rH|~AaJLQ@OH$TnRO|cr`OP)cn!w4x zNl^MQc9Q#T%^-*IVaMtP%3Ran6CQ+vfjkX+MfnkxoDo1gKEt2u^u?T8*f^tI5~O4E zc~$ULGMzL$7q$6>bmNy%;vKue)5KzgK?d`<5z1oH2}%M;e>cvan=VP$j{blJ&WX~nlV!jLfs;2t`aXyR!ZiUdf0 z0T~kh2KBJ!AW&cFo(kESwPD&H3|r$8Gzjg^1v(uKqT+B4?Pw;dDsn#*Fo<-Vhh>{a zYKv1i+T*+j%4Uq8{}jW7iK%f#@> z>vCr7diUn(c$($^V+{WbVtCbm%*d(+jG1ON^SNzs`AT)9Z)HC?A#eW@4Al3%ZSv=E z^dmyHN$=v!87_$W5V#5&Ef?Is?7Cl?XNWv_yCP;hyMvga4kd=4J$L`?pkq6-iY2Hg zn!2K=<3RM*s&6FX2bBswHHL3NtLXHZFL`&V(#=jO1#&upUBl*2_7uA-L93M8&i!Y% z3tq^yD7mE^-sFK;c21gT@}YC+tcBDC4JRAx0%}of2!0U53ZMP~+MuSGy?U@ReEIP3 z#hBl%$M*=J9Ygi}S!U&Dy?Xk_4GDE9SWsvvrReeG-8zX?alw?U=?I4K^mGnK6_ZT* zo`lE;yOyisCa*_t|27wBcbMohKWxR%;SBN9`gHt)6q!uy-b&+&sb$vv$=Uq0S!LNh zc#%gRx+Oh*s>P-np-6X$N1;$#Olt2RM|p_#z}UM17MLho4pX^Rn1&?rcwTr~oHcah>V;F-o9%MJtgPVQI3|bA9s6 z1@GLqHG$2|wig#DN=O7M4-&CLPSn=OComP-{ckD=rQc|55U7R`N$Y0{Rm6iTsLa`| zQ_tp^0;S=36^I_Lk$eIDU6(W?>jRHa{g2}dv{77YG(6e`cwqP`|#R78yp@rP|GBa{A*?Q3LKU4nDsHbp8fQl8L7PFe@;t zoz@H(t4#p0zR*P;m)6BBSFetb@Kr-SJ_a)r?IHgd+ry{p+TTf)A1&~wQmtQic?QN6 z8UlX1I=44XwxCXD{3Lu{KUJz~0JwN2A`(REkXMFMG9!(M#}<*w+s>;Z);V$?jpHP- ziMwIU8)Z(xZcKarm52o$YS!=kq#~%(hBHsFnCLQTsVvIU5UAH$rmB2k-SQ`5w`%?TWP1>P3sqn z*$gauHZ3SC8xsp2CUoFIOeXMe-(%)IlLDjC_&4tOIIQD{c|>f(Xf@4UKTf?FJ57$J zpLT7Qi`->}Ebm*v2%A7Zg-Vf)X`Jz`ETo7O9L`r>(>inEW^|PysvxP@uk{jQJqO&p zgwD>jMBE)RiL&t&Bc%ayG})t9jyv3YHL;?xzuk@J_v2`@uwqGkS`bIzqe&b#Xu&G! zyG{5+E}$Kh*iY4q^H59GEv&N+iUiHC_`D+i*vSq;5l1jUr9;}Op_~mtEKf*3_(XDr zW;LlgBw+}a%Zq z*E>SZQ}othI2rIcT_oP^&fhcbx=n@08`CT3*k||{30WB8Gk@O_d#!KTWA96JlGut<S@W13e?!brxP;&Y)DHV}Jk<7J=ldz+5UXhGV3oOkXf9$!l-VIasgx9y z>6-gx6Yr*-?!M6_U7=osoHd4%+_`Bi-847JsZxlvVG>C`w`D%P^&uQjLJT|Ctnp!3 zfYt*RjN5SwIk2fH#0mCjiv=~mFiTFoPu#e!qH>5iMqc%tFQ~d=`g*H0!(ki(9$-wz z<)P~cCSD>=W}M(I*o@)|2^Q>c2qkct!=t&p{70Dc7ab{3+^D<2cA)MWCL8qC*+H!N z` z?7i%VwUq(uR)+?k-D5S)dI9ySpB-jnW&JD5EE!{rK;n=ti>)nG&i1!kDsJt z``{nMSbdkY;gDdE-v6|a56hPU#XXNQ4G$?GgV+d)qNVi=IoROJD%Vv@0#iTyf=x^n+b8U3dwkz(SksO0myz>^sl52wQ!k)g=3IH z$N88(#0e*XWS_J;-t{K&PrvlfkH=w6mi(Ch%>JF`Y;KN;Mc?OO5cz@o_F zK)Z=eT1SuUap%7FI%u4Pq!`36*a=azC9q5LF%`rY-{rG^QuzeF6vzj%U6@R+Z?&O2 zZ2(?ZHyx_MhEoqt90&1AWqih}FPe`hJy5QO+S+k-VG{g_WBXQEsnK0Ne$zn2<50Za zvqd-Z6{k^wcKVFWYq{D>7{Bu9;RU+&9MH=dA83Z~3%Lbp#cZ6=Ui=>D?4U~KD~3kq zUteV(i4xhAM_)ur2MgjcdhE?Qr3A>Ra6| zEK8S0Vx2K>a0?q@P{r#C^A6R}RKOejd>+F%))#LZJseDcow)6p_K%W#nk5%KNg$sc2K%Bi>g+ z5%?F$RypWy{5-~N#%zb$_D=gH_g;6`*FG$r5E);lAov09PsM;%&jr1zd@(2ytks3v zx-6a4`tq3emfVQpYlkmPQGzOX`(Z`{lph)0-khm@5<~irm>~L9pT<>mSU7ulX+d>V zeY5I=3HSk-Y?+RxG%n+6h6!kGe_ZNxEu_B;xhp{r-Vt0kG znrHmoM;UU!>s=2L1tAB#?C3F|u2Z}y1oe(r)Uz`LrK!`SU>2P_ zd;YO5L}+(AOEV=vtC#71w!FyBXo^^6C&bkj8$*jggsq>*2wAAI==OI8bgymvxHR;# z&5dga(ccQ3gn(^jB}GgL!-_PjK_5-V(UAKEMZDnjSm5r=pU!d(-^y-yt<%N!X(1VP$sXQ6uAW%c+PlVd&|$k3B?p0e4Ph99NnGgIO~BRF=kDishuq9AI#} zcuO#r@Bn-Bhc~{?d%pg+e^{@72RCv0BDzdYw4ISqRLbEBBa$Tf5C_x7L0RrElsF(LJG@MB4d1a^?ZOuN>Q(84<4h0_ z-}@iYe9Wk9nLFV8GjBzk=ify&QR#oRR!$?7)j`vMI$DeC_D67gEI46^-1$EDV{O%@ zLVW(YuGa=(^{>(U4*86lfu+jg z_mt_9Sr#hG32kw(E!w+=h^@1hVoWn_3;%2`nv>tVZr1+>3&@RerM~l0Er>*@BeXE} zO>D@J8ux(L{XK?!0e2Y1te-g3FtJ=QKEBM85M$oy}@ij^{JaiIAmzC$TN|?a;#Dmpg*xF|T3_pl$@DZ@AZGyldRrIIBE{J^l!30(Ro!8z}$Vzc_bi)(knt9lWwk zX>m~gl{00|{&lf_Sb#$%x@S!0Ii62&;bb5=U^_qV!1Ov96!uR@$Ev`&WFn4+ff~}D z%zAa|jZ-GhB&>X5VUXUvTZ7NosMO#;uzhvEL_O%yx8|<8!z@nGhaBBq~PZj~q0rq#}O*VV#J{ zssTF-7IdctK3aTR$56bi#%=_;fA9eM7~Z(-RsTtgDkTSC54G;6Zyh_ErBQElee!|IA69C4RuQ zW1Z!Bn|h6b2j%QwTM+etWv+ghGuiX^_(>1yN^K6B6BU0%6&BRR)3rPON7#RIRI(*>moiZy}G#JkEkMAX;XqfQbAU+ zA$tki?8|TXn-LX$J;1gXvM>lDWt5@Hm@-( z-_EAENJ={j?wmDnVFvL0185!mdyU82l5+l1Ng|x1C>6>ZZQ|Q8I7@a#+ilFSpfyw> zUjR(~1v|13pCxoM%`nthlPV5m9()umB~A%z;Nh)M1#I3{=T~)~Fr1aMiaulx zTHrxTO$^PsPALtE^xH_kK7^+xia=Q8>9{LR8bs{WnyhnZ?wrpx5Y{z`N66Dv0o&4< z$%{RWc5FNFYy*5T%SrSkA2(YjPg?gsLbPu(xlsnI1w5b$p7V}bvzbo2$ z*08HUq#B!YQP-%*-*AC z<`S_>;mcvU2-y6Rzr`u{j@rIa4L~&I1Mq(OHM+3lB1;aI2@Z=7|$7hNTjp(q_Zq zP%b)qAu+`{3|B0m`Od_%xPB*=d>F2r51fx?6uvr-dT)}YW1bZf#FttX@olGb3Q}MM zMX6^f3#R2%zW9hic-Zd#%p@05{Z^Dh>&m;2ZWGEoy|XZ;AsyiF*Ri9x(8{6a6L$dA zp^L=Uk;XMMlHnvBH6{T-{Hnz94wnD&%d;{Pj>1qhFlncpc`&TJQV_;g_|aEDI+zVi;GSsmXxc;iVXeG8WD;0!&i%4 zwct;>{c8Q2d^8BY?kE%Ag*ZG3~%t zjxVg_#IT%L4Se7L2hDiyz6!Up(YwG&V}rvX?PVJ*2CVqsq3B^jer~o1GhkoYH}}gX z1}PaCjMVU;U3y6qL2^kvcWdeQUAK>pZ~1=4H_S)lwKAl`DJ;RJc)Wg%$_0399ll1! zGEP;zQgfvVM>;FluU83vV?)uf9Dcg^OPSs<4^yUcb!N~2hy9|-95Oz?I*T8lexKlC zGDuR`gCZ1ZOTlu#e)#?IY|<>|Ibu1qqUSN2XN2!mXBFEpT9Y%>D)+=eFGc}!3tNV^vJWVV!Mw-cp zA@DekJtx$pabJ=e$J@w??EA6A; z;Ymy|(UR*)A>vPMT}uYL)6hw&V5F5m=pNN&JOTY8Qk~r4k|uz^F}Sd2J&eKtWQ1+|m$w@ck_c?FL+s zeY0nsH>(p!bUtc7oJ&>L%vdtmxmsEWiJ+Q#J zUTAYVkB*SWwGtfcw(W9YogRFYNryEw5R1m{LYG3x_6#gQ(pgC&kL@yNqS=oo1#kdD z$&)j1zvz612-QRsmv`Pp($OahB{;m0R|vgLL2E}gSQlAid0Df{79 zyoB?pGp+tx4P&08T^GX?OIImz24!ULDCfsNAL3?#k;waLd#fTf!-=zaL;CHsB z91N&G$JB*L%vp1vOk`JDTH4JtS=!NZsgDSm3M%wQNG0)(L>GQntNo=xFanOq*RP<< z$Bp^%{5khnIHq~;~Z2C+v%hr?ofmfh}!HA^>%8NVbhU>a(o zp-`*%K>0NZ?W4aRUOn7GqFr9~qt@@u`9t$ytO1f25g#m>j_aQ-UMd7(O@S${^0c?a?QNwI=~1JB zwAylYM~Io{6Ry*D=INTe>BpdJz_PmP-nld(W!+Qx+v3BDdI%4SR(9O_`hoH8Bd_*@ zM@fF(?(p#^KX3m=K>MFlGQO-z>Fa*NrZgFX8UFcsEAyMAc;T`bI@${ZhcI8?1l)*> zFSqUJMsHh(umU{SG&fEpz7CDLxwyaM$3{fNlN(uKwso9If!$UH_108iRJM4r7xXlTzG)l13-athG1;!vLgukni!m#5MlcTc&K`+V?fu1GU) z|9pZgwGoldPur8=X%_~^kmb9IM_`(!Xde(AHVk0BC9CA`i}kf%tWfA+7G`?ol19Af zFOWz>g>2eTG)t_C8rEg5#i~*Z^1HIcmlfX2&YlB)CFpJQB0~i9 zQ*eq5?JWpmz|)%-#j4;+kKk_?==UYeSF~I-t&?#~)Sr^W##UhT5CU-+n3AO)CI+!% z^i`Z1lS$=@mmM4VQAw`V#{!xPZXs_{wM^&XHh zOzA_-Ud!KS>ih5zUGNaT$%$69h#}O{E;4T05;xA9EC@8#Q!_N2vIc_g^|={0HMFey z9*k@PjB1v^E2Xb6eg~)J8Yp3~HCu;;%E#1AnDFiYIrB~voOEb4#YyW8sv7sq?59L) z(zIa)sB=OIWi)q}nWuP2&RM*!ltGz7#)HT6MVRr>h|G{IzXd#_3pRqkTcMI1Fro^< z_|nG|kQIsm)xRbVV7cx428nP3&74B8T&|=yof$dp%E3p#Msfz~PJ60W$M+X3YS%Zx zi5PnAKIHTW-xK8Y1omGSvxd?|NoMP2-2iYU)GCQoehj+FRaze)g{}Ak3RCegw^+AW z4|&f#9-iMOo?hoRokBis)k5U_a*HI^_*&rV3C2MAIZ?7upUrT{te9}!l07EN$Blgj z0up{#WznYu3PTz^jVO{d(OK}LY7jgcj`?)M#Ah&*PPvd%Kv$r|p;cB4Lz`fp6=hAys!Vp7;&s$1n+B=VfFgi zw2*Q1FfsM!4+f>4^`$k*SR+7wznyvo#D&x>$gq1NeL{i^)6)?adsycln9~eR>!4>R z^c>VSkEu$ZaAsqiVP|9HZ(Z+O9uS|e-_8U!RwAGv_A{<`K({{JZrkray9>8k?!g~H zC#!sW)>afTk5M~uT2%ABShUmn0IwrSqUJ0<9XBK%I`EOcCut{?Xt=%yftO4f2EL6T z)9EslX!SU`7Zx86G>MzXJ1eN0s}T|!;(4tSGG^UWQ-KI+D*}lih7gbl+EWy=2meZc zcPf;QAp?qvO#<}a(BDZAAI!6l=b~sw#?<4{2mz}$xG;zWk@%jYhggiglb~o~#&o&c zGB&*AN?@6x3vx{-DN^ptPDL;3&Bp?)-aiJU_;UES}ZF}Fz))#Bxr++~OuOXkH z4MqWQ{#t{c1L4QlbjD;PtU}#r&jpm9?Wt&WM%mHVd{DF)%*D%3>=u29*|Ow z;qWskQ|jQHyq8g&ZD&=<3btYH<^Z-q+#!JS*8*X;oW3+fG93fdfe9M!7V;mRR)z8J zCm%AX{@&JXyM>$qt*1vQa{78Ru}XmD>B3c4-rW83$dpds(&~Pfv8<@=$Ov$`FR*^V zZYu2MYkn^em3{MCB>x%@|_<~{$Xk9aCc+CbtJ-H*W8)@nifWlJG zl4(maO}#jv@=g88&tmg3-E~WgDgxp_u~dep%)UopLkdUSo?K)>LIs#^wV;@pGbm;z zkJaE(A28+xc%fFULB>)CfDMV-1}Ajg({hR7g#1{}SS3ZD@7n)q*$>&ZYypayk-kOm zh7k@47KgOWV@gtv7ikeR!|nTRp~3USz21r_T8-O4+MD*8OuPJW>bybYWUkSh>VfSw ziMLJBh*zL`re8__DgQkPD0^k}fZfurXz31a-OQSc0ndu_;bxK_K!t&ZWD(*4h1sfm zHWc6^A-Hx+P%sG^QP3_pEE4}%zt4Y#K>&H6>%8`aB@Md$r1RvXOz1^f`I~Pk;usdu z{_?uD|JP)s{51S~O&|&k$(bHe1M{RclMt#91v}6Gvyzq?Mf|P~Ah_g^@C` zdw4J0K3gx^;aGeCnE6(Pd&uSVw8(3?_o5m5AywMxW7&SEr0Wr=XhwqoXt?>SXofk` z#mTO>`Ls~k0&vpj7!R71n~*D*w6l@2q@wDQ9J|Mjd8-G__;M$QQ73~Y5j24N$xH~f zm9rXq;8Vb~K(=0Xhcc?OE_p$eNfs0h7>xshv|K;H;oR8tonk_#qUO+7B)Qu$&}eTC zgVW5B0`v)KTr1tSkx~uAQ?$0_$+Mb%m>rJ^o$|eC2AVze=kYwlTXShWZ*M`7PPQ0J znkR+!S|AX@gri@0=S{PYQlvr50-53*E~~5ZEkFi#hc)$N3|wDv{U+(5>0Y!4+M3I8 z3xKTq+Wy+s-r_aD%o2h(J3gi6`!e20iRC(9bLvi%W^ zPTTwidD3kucU`zSF>9F{3Kzi(Y@~IN?P_()+V}Ykmwz1^Th%AvJU`8>apQ96cM~pw zBc2~|;80E~j7I|D3ASP-iyR!6g+>%aI zeTvLDgK>ms*R{IZS?i5~&Qu;?i?noE!f!4xWdl(o9yl~aYu{0}!j&EnHEIQcC93$9 z4!>>+Z!TYnCP~BP|XZh)p&$$S8x3Qm^%Bu=XzV)*7N|#3pS9PZU)fz;Dhl; za-QtCdTAuOxw&Usd+L&if4qZqR=;# zx+%4zHL;DniwI&T3e!qi+N>fCH&d;7P!mqoXLAp>Y@X<+duQ{3g~I*gYC8G zwP@B4_vzJ6q{vVt<`v0^f(IDd_ae!&6e&h!eEnWs%{`ttEe?mlkO-niI=5E4g7PZO zN_t@#xY~EnY;0FYzQ4=59*O6Xyy$r2#GpZ&pj{vVqD=Z-KlyDOa*%(~2WDkspQU1H zv6t&;)-LSmyA&MADV5@rIk+^75=ZESOhT-H<8vyeh>>qRTT&olp8?92!7R#6RtI0iz!o-tr|TYRV2#p8#HY*Zd2d1$fFV?HV5VYK7r14kS3g3dhU875MBl6hm=z9 zj;0nGz?QM9)*#9QBu(Pr3)1XU%c*mkj46J=Qe2oaLXx~iF(U7vTrRNGg-CQkH4i`y zSX+L`etok=g&ciMzaX%Ue>5g3E>NMbzxYU*Wxj9{KMN<DrcA%v#V$&+IrrZjrMqQc)*2u4;(R#YOX<{$aV zFjj8RuiVSp%j&WwKT!L#;{0iUeD~s2rv?V^pnM^1Z|_wpxKO+GAbA1QLpw@I9rtRF zv)~1=?3M;i;2gSPr&PjEnBj;QDOUwCVmY~o8l7eE@Eis})-Y_Nkm6^2Ja|hJ8LrWK z?)ZZGffQeb_UkhoLa9%$)FN!4IGt9Q8to?`S#?^rcMF0NhJm2ZM3y2}Y@Xo=&It;l zfd=}+Ca@i_a}#ryzrt%a!6HC2WemF#rHq3D2zfw&1MJq6fBh5jj`{!}0mVKepsGBIqk?H0<mryr&Pb7rQs@)Y+r`j|O06#knu z#EJ#^X#wJ0_YD5C?g{)`_dw^17^=5_FyiTPf@lUjTIPiWiZ4SVggi39-)=#i6qoue z$p{YOb7)*8;)?!Mk2Y%tD7MrfhLZ&2`@~Dqq~9O zl4)lK;`L)yjv<*ky@wE%hzhFMQC9_D>Asn2h#Hv9098j=HVAvlKXeBQ+rWzs3njL7 zZk7OQysq|*0d~-$Ft@TN!ok9H*m{3WQnQFvMBN%WYYu41Db47=MY7Hk9T%+qSv!D} zn*?(PT}DFcAHrsjRP>x=_xvG31#4YfB>H+SqzDa^Iw`wm&o+xb+c^v8(S+;9Df=MD zy{o-=2W*m{?5#(EYLrMJA5ufhr-(Jp2PBN4sEP_SLe=gNSNi?**|5H2xxBPPRSd+Q zMR`B_&J$PSH1c5!zT4iyyzXCj6-$J(T9_KAs}{4{ql?jIOeaZUp=`_{u&X~1R-K)Z zLdb72QTtNHPhKK_XDVP_rbRm6==#2L-!kHeib0enV?w`d!hq1=Ovzj{E;s15 zNQj(&z#Cy9d$4ZT$bYw5kZDgg_KbWBXQ@Frr2vUNzWE`X76V!7;APz;k}Xwzj!)Xx zsWU{}3(OdFoecJaON0+KUJI}R5XoDI45ZVBWAsdG`eETh_f3dq)?-PBxRaA0M0gKI zTg4HK$l~_bLFh*d@m}I%Z~G5>Fkn#6op7FbyNAoZw~Z^vBw&45PuI{(1o3G+KNbn% z!|V|2rA(O*iT##O&+bRBAP*zh!&;|N$4zhO2y5-XnlldMu;o$h_pwG$1CR@=b1jE% zMtw!tEH#ZG)4|jEA}l(19Ee2hjov!D`N0`M+pA%wfQuuxw!`(0Vc@}OMffT5|pY%~i(24h<-eo|@ zQiq;U2Fw)%J;45atF-Os&(AvRD}jT$Qmw~_sv^>Qy`@R9lILa43c3-QQM*Lc8P~MSD(EEI%v*W5vIY- z$`ypH0SxSyWpj(e?d2ySSVo3k13}n33{4Ba zd<0f~B~34R1|1@@>8eF}ki#2hzpdjeTs|pfjVZnxl^aH+mA~QJf{w~B2mHeL*@tMI zkq&0IHUQ*4S%2Z}L4La^LdgJJL#P1O?`i}R{Hm@FtKo-i3$){?(x{414y~^Ug&YIO z85UppM|S|Pcct7PB|QP#Ut3A2=jr9K}X_*0}oRVI8cVR;*A+ zk)$G1iY}(T-1n!CHRGn|YVs&#n5y9;mqPF$`yr}TGAti$>;^e0QE3fQ+MW1KOh_^u zaDwZgUBJFOFT8Vkqe~zBfL@Sv9gKB(#1PVr&JYcFx;cb(5Iw}9ir^*%3{FzgyBz$K zrUi2pObS|!3_`^Pr-yboPuV*%_zjBZqG&q}nzbp}7O{exgY?EdHGUqxwiiFsVk$h)@FccLJ<1>If zmz{=NMhbW)SS+#5olsJE6{kVo(FwCv(hW7aL)@_Vc~kd!vxa!)qK0fh%%{0Sd7O~| zF+Zaj1S=yvvUJQLWX*8kxr1~zxcx4f^b2#Xct|HONMn3Q`<0z`CN5E1tQcyx9QUlJ zol40n&Gj-YWchNXCgt^T*`L{Yk(rpphQ(Xq4?gAzf-x>GE_a|qX4|Ssfy%OMm3s;JA84HaYm5W2}aJqX`rqosHxdbeW=5@qF4VY+aYb1m()l2PalF3<@rC%dUjQrQE%ADXx&(M z44Pllx6W^*z_5U6foW9_e6r$c-C}x*%NJxOxe{TptGet#3}aO2raqj6mcU`$8$%ug zbRuD0`c?%Gvb=t|H^0%U)j4nbZjT)C(TA^XdkTohM$AqGEqNWH*1IUMV(&*Ew;LEX zAkLt1rGdUO5B60@%k=u!&$%tPt{cu(f7Upa&s2M-mcpB548)P0R9Q;^u0qJivJ$!jQ*EUXJ_T#nGW;Te`Y!o zT>IxdZZax(9OR!@z`;w)m0HMRU+Wo0zVX^8vqeP6wq3N^mEoO=Q_n+2oS%6~>EKpoE+2VV* zx8q_8@zi?C3at5$m^MJ(FY~?;(^sBnCCVS)ov24GqiAPSGybjPI`J9 z<=;uqj93-aJL#GFi}VoxgYq09 z`nBr5-w=mxnTmFghNqPxhsM@fOn8Rb2sSOP#;F@;%zJY!Hb-3pdl7j@)5;q#I~z2= zFTvE=ZEZjLk$(wp^+lV1Gl0#z2Crk;@z!zCck+j$kOu9Md|8z8DOYvMr8E_5QxtS2 zpHrn+hCLrRzrz^r)shO@LIOPjJtQ)jG}aaYHAWifrA2t3RL;0Fu*}(%$R&u&Z97aYNc)`k0&hw z8kQNG)_o5+kFbxqP3_6R4`O4FQ`n}iPcaXx9;Yy#Ufm0t3>PwF@=*&->8ZR3&~cP1 z2S0s3OJ%~dPAO-&>>sm>jqfoyfaVSpAsNzUv&(H)(*v0#4mf;)!Aeg6g8qcug3vR8 zc0TE8O9oEVZXq)atg!xu&QJxT<#@u-s42vG%wC1(w%ZyxYCa0%BGTw+D}}ZoKV_m& z4Pnb;G!E%PKhtG3YK$0cU1v+zk`x(gp2a;?qS5#V%eBc@3#Zj07sZE z2wBsz+4!9DZkNe)_uh7d=(69X?RncE{WKwjrx5Ge&%XXTmR0o_78LkQjx{Ej4*S0F zP$5P4QPPC;L1HE^sIo&rLOEV1XfKLQ%vCCND9}oL2RsUz4r%M%F$K}^I-O|bQHmT% z7D}N!+d2;*A(Z_-Ki!e_)jS4_MTq^WW6cQ6O&d@m`_W#%CR<8M!r_HMWMbu>CwLH~ zzp9v!OQar#uRoECRD8A-RgN%uL!mq<@%8bvXciNF81spf7ZfVej1lT;?!0 z|Er`27esmh|5v8xE)ny8lb%KP|AFWM{68Q){}a&z_2~(odHmu3oP1x<1=$X#?F`7-Gk^H{JH~92q00nPWv!E4~ z+Hi2{R_UZhG`8%Jh$~&ZdN)?K6B8lO6pOB(Ts{gwBN(|i+Cg-J=fb#PN02y-p!MU$ zJ2muUz=Bj5UZw@@mUWXe)|ybyJ>lIKN#`zRk%qO$1)>uJvZhZP;fmQpvx1M`>yx2k z19Q7h2$UnK2?$ z8G}|J<~UQex)WTFq>%E=nhTB^EF3eirfx50s^g(2KoUf+#l<&4H=E_-)9VxFI<5fbTd3>;k~fuR=r@(9JGD0om9;2uhVa?DQto(&!rJJhBZ zH4g}q*q^D}oteR3zN1S3qdoY+nHVC{hw#P+XsO(EE!!m(n7Q7PSc;9`-1MY9sPp># z8-QwO+G2Wbv>a9+QH<>IJwJ6n4Gkj9!4d9JiCUBv)T0t-Uv5_a{Yn2tVc$|A>whu! z&Ow$n-?nHOUAC*cj4snB(+?lar zt(7@*%{g<-G06PgzK{>EMdGC~fCD+Y^};DB-pPy%UA^dG=a!20pbSECg}gZVwM-EZ z9gDzF4m2dIzh5lijn;=k)Dc@%p{aztLt+pRlQSdLnlvt>uG4!(tCr~E!SlBpGa{`I zNa{7ptvp`iw-4N)q_FG?>9rS1>Z!%xDW^a1(Enr#m7~T}rf!FqGLRGj=L9Uu3?)V2 zpL!OKODBm4=Cz=hRQ`DlK*dWpCcQFlbVpsMQ(2h1b!icCfVbzgU=EyQg06Sh1w8;g z2olc~bT~eJaR3Km#Pn2f5=W_d>$gn$YxA*jVCo|&UFy~@v&x^|Nfl;3N zX;Z%4gk8Tc^~Yous970prTzaWs^|X%_0U@E;&9Z<%Si{31lIu=kC`oy13l4ToYreV zn+;vywCJ+K(L2V@2>t)A;YrOqI==(=_3 z%+y)D42U83k`iGOiE@Z|xlN@NM3nTx6DE*_3CqjL53q$~jYNZsAj$d*iFTmSkiVHW zBBT8eP>-QX(DR63{2w*x3LRYTpU9$=v;xGZrhuuyXZFz~Y+|doLW4V;8!V5`kafT5 z2A5-);L!|g!5+@8dx6Eg3@T+6WG1&SqSp*0L2tt4Z&z7sYxB&cCgOGDM`fCm@_9}Z zjBey6H`cV|Zp9j|U;(Odf>QRog=y%aKL?-R+*ywcg>n$}+BDQIlW<4zbuAij17{pl zg8<#!P(22Y%b0YZYM3&=8#j{=-8N@qq(YqhW6K6E%|!-9%qeR~mo~jvpy1(xamD$Q zU*_z<4KXdNjEMPJ$w!_u&nFx_SmD(L`u2^2ot?mVHHPEFl`7&)Q z*v$EO8oRK$Inbj_13TM$EKQsKLHsiTm|fOM28BZk{PZ1SFodhf^mocwV!$ySgVGxu zt;z2$TB>}j@{#~2h$aEm(C6WnK?6zZ9aHXB^Sl>L3a6IU(QO4CPrgyd$XXm|m6IX5 z(HZ>r;>6$XuSfL7c?#o?m+00_-n~)7h!_Yh$X2BeY=8|N8xoP39XM9+t=eiMy+XvyPFHD&A%Vr9o#)6 zYw)fQkbHe>ok##opAU6QVigM>@t0}dZ+*03g~1FkQvrM3cf4cVi0ydi0Oqo!l|_mW zSQ)*jI=7ERVOYXu^dUORn$+!0uICm$cJ=~BB~Fi2wJk6<#u=LJw4gNT1gz^T>vh(KZgn1Q*~L*{ zQIkO}&!Bcl#+d*~Obc6gbYubNOPQ*;1#W8R78us|6ZWA~8TPUF`+H%B4Kg2FT_l%g z+~-{H<@;ASt0~g9EDQm=xyL){bg%2w&n+-YY>xj8kHhpY9;fYJJPy%cJdOmD$Inf3 zGQlLVnhkRoLcaat1RqPmGZ0Z-l7~_-MxImWw!x_3 zZ^&OnTf_`O4KN$2Q(F|?1IU_%ocXeNl9MQ{$AS!)A z18f+t5%D;f1WM_VbX0vJ`4-NLIivr&&_~)WPg}n{k8$UtbTefCK!MhQkvJ{qSflMZ(NPm}u6NrLKn^dXitV9!VojR4Xn&55S_vb`Z6J~4lI z_89aXpw6OtImUn98b=vJ%b}x+raF?!5sK$UHNgqWi5P_>Rv|B=Ej!KEfL~p(nE6je z@-xCZ9?9XKI)l>nqYi`@lF&?G(A77=Lf)=zO@Mw|oMqV;Od7tC^UDt!>-4-3!61_U z5mu@(^9LpwNXhy#eeJK6A|pn305LM3T#h&(Vw(h=MvO{5AFBy1Px#%Nt5IEY7iT_? z+6WL6^5jQ7-xX<~n3VM=@e(f$JR!>dhLd%O;{O1qItOhEQRZ0piZrMT#CKx%8NlB2BmGEY!?lOjCyl`gX2>Wc7U3iRZ z2@g`Wh&=}3EFc#s%1xoDtctfXt|MwSO!U-L(`2A}PfsK)YXmmQ&qIiP{Kr$|cHW5r zXGdXK(pE@SEc{V9E5b(g(CkGIl=}7azdn zXd&!iKUFyHFGng4L3!Dd#tiMypg z$jO5ApLMW;3QZZmh-lIhL&i?kyoROQqSw z8QUg_{m_8?SZuZo`-L&F<}`*97ubAbVdCLisVPS#0vF1Ij^ zO{OyO(G?|YBe)_EncZi1(3i#s;wioqYDeojnpvW%@BTS)0Ow(R>}^9Efx3u2%S0bU*^SUsT%HLhUU48ay*m?gRyCd(XBf8@8ltH&JO^bpc?UVE#B>Qz zJ6d`@G&#>p(aQilwI6q@RVm{r2vbQaSP_MnsgrC<)!w-rbMz($%bQ>q6`oBXrbLy* znUIo8X6?+4nu`oE_ak8B&qdV<4W&lH`h@g~l#9r;7?2G`K;%WjDwd#xoLzz>qm@Do zjX&C{S+RNsemMFfX0%w*yNG)(Ey&f+6J~V5o63$#Vfp|fP}9n;1spYq*)e1Bq9l$> z7&0RbaBNTLBJ0v`;mhJ`cXjRNQ-V@4o)e{ADS5s;Ilkm9>`B>ApRsCYd)hF(T_p_g z*kjT9EC`rvjW~rV4U4PKhT$0(vFrbX=>Pr~(Xa9^qF)Vc6a~T943t2L`pes&cqV3r za)71hp;%H~{w3UKxkW;JcVhDHh>?9qB`*bs$A1p)d{KJ%C}TkBC~Qn?qmJ}(Lk zUH^#&%@I?OAX&8ct(a$j+aK_65YU|^lKJ>I2tfO7lx0dR{w4eZie|jF4%*z;tuD)) zx&KbdoEcWGK9NLak&i+(4zF24USBrm0LC75Wk;|!IWzIss<-xy9Cmo(lzinc>~(mU z#2%^4F5W?$0B`h^OJyM7-{9_8TeYYNqy1%Q*r@%3VtmMwjv`5T=r?0wOB=}v)TNHZJnjj1KR7v=O#1KlAFBxhDV zke|hZ{(?^J$I=OJ6x?U*dyxk36rAJCh?t%xP6Gl|L_dUJ>>;~-UyBwVjU3Sla-x9F z(48$}as3851jy6C;|yfwMMtZds8u>dw~XO9sv~-?Wq&(H8Xg`q`m?@3k_x7MWx3Y%1ETJQXz%Dc@e{% z8f;D$O+-YW{GPXOdxov*WOwI0 z&^7v*!bgEt_?MA%q<~`;(2!zPbzy_xgF^S&g}(fQkBdxlDdgtGFaVD2^}00uf&>?F zd93Gnk!g#ML(kUk%&msIadL3Q-}VGS`fr?I{Wne!`|mhmQfH*hq}_<%Y?IA4BXBHm zFgDe$3$f|qlg~C0{GjIhkDyXJX$n-DNA-IJ3-YlsKzwI~TddX6Mh_$RdnCIR){&Z= z*}7-pOeIOxpE}FJ&;MVg0-)7mHxzu9-(y*FTnfqjO-K;M4Nnav_qEov&Jka9xj)v_ zeK$BcKZHAx*wA~AzcRM7X;w`}dJl;J#u4438$?`o>$mq=eWq??buYzUx1_1v#83NH zfc%$?d^Ki>U)?>a!kF{D$6k+au^0Osr*h%jnA zggrp+%KVbv*sMk!cACJfVqYOwA+b zo(6;&RU=UD@0!Q>mMRcZXPZ>Of72?8^^tE}!%5><3M_v`Q9orMfY^ z#4^7sZSpy{rGRyBVvu8i(*^=TliN|8K&B2J3?|fMj#%duc3?uP}2beYIX<^6( zb$qw;OEBS#>yDC_E6>tXHMYU<0m0d#BbwnqXx*7VHfGA7Y zO%8wZd)Y0{HzPKk=X7-iXsT?j6ptlXkN8eyq!;Id)XTCNkhSb(l*wc-3_&EM(GpIF7yR0=go&RB#KE64}-uhhCIzptyVoNimJlpKM) z`X@B$fDzkGyrGda=lHkbUl7k>nci7MN1&EWptLrv&wzrh62ZY?&xP8q#g*CrbV)Ts}#8bq92@(kY>Qc!@^^ z$NV7Yz+N##z83dfR&>*gLQDUDrl0_p*P^0K<%T}jJnFI2q zsgx@AQr+37$7s*)u;_+1O=hJ?V?jEiw%(hHf2Y5C&Akc->K5_}(6h~qZN{Ev7F@kN z*&mmuURZ)t0@f~wUW5R^1NfFb<39p^Vj)q9w zm4)yfg}8Hx7q!o06T8@BsA7El4JD#7jwQl`p+f&E|Aq2)xax_LEc?WC0FqSDjw-T~}1UNlzYt24O%O1VQD^R*{g9cM=L5BGfC7RVX%%oQoub zIvJOGq4V8@Ap=kf!M|rPl2mQVKCY)1$S?$j2X7zJW2;9W!$#=JrSSL+06@B>gqOg;=A1udTUxyW3Po~nT(}V)kIhb-&|fM)aZ-X zDuPL+vDv}d32#vE*xVn|?}R8Gg+a~KL-vfvo!=y>;FqR@9hq|psrhL949^PEOdrc# z?oJKNOmeIIQfmak`j`n}Vf_+E(Dgmn z#nAPNKEC6?ZP+)bIm&}ZO$CM;jGmeZ_oKo-U6FN*lP;xA}6SqusPw9`fqHM`HSy9%|MDk}^w40zd6ZJ2Ri`?zvKQ8Wey|3M`ux zbV^@zU2cT(MW8CHCv`Wjv!h1D)q^E&i9u%(j)fHu3Sg#|Ic$g9ko5xDJpD{f7TfRs zW%CTr@U%f#OiUs~j1#;OW4Qm9=Y#GJYh zeU-O~D_4GyaT?(P8bk~EekMgJ#E)-TDzF^TDP*de`IDjjO7!pjf+jzi)=I6d&aVz! zZtagBg3kO?ckICF5L;)CH_v5muZ%P2K@40|3GBhS0MhSEQ)Z6dSE&t`(@WK@+Z8pV zjNwF@2atNnxM-I?1aT;m?M|vLl)dtMObVe6TNoj@sh4XYOuz~Blir(p-&?pm8sa{2 z0cA=ap!S)8>KH+hIT)Z$hGWtTF$&8a%ANnZGlsF(!+44O&CSC(&M3f{g5fXiCcY8c z5HMA{{&elxy!fy}06z3{??(^5a2b}TDL(W1f^E!5*p_p+5l3_q(J?-nhdm9MV^oy` z7uEOUlY6X|wl2^iWmPgo8Bsk3cx7^^Xs*#=f&znXk$8>wKJe8E zMjmN7q$XC!l{(7RvVI1}%UEj5hF<%0^P3-?pgaeL``sxouk!xs&wXDB93CoYYrMtblnT6ohOUNKMuzuFlPvu{ydId;=Wp6{{?2T%kte6GK*ohhfOKx zge4yGG&7gwyG~jrF_$%xRT5s*25*EBLi_V~i{yHJZSSgUM0KuN#`ps-1f-=?V(Lp4 zH*2~h&xsp1)#wqRLQW@RAyXhzLl&BQI-pkzv`5qvs&`zDOUDFZdhnONx_y{J;f~rG z3GGTnb^@mcaAUiA`+DrUSoj|~nCaoBYjd%Do zP}*&s?Tj`~D8Y*q7K z3(FPwqd0fZJHlA>`C)&FQsfu0DAQ!J`XhXN6kK&T07~T7%u;Ao&-Ed3x}B;E0X7tT zB2e%e-fGt8=IHYLba7_^l3{%IALZ~sNRs%5iQ=E%Pnc7*sdP8yY-tKaIq8*}tz8+_ zP{!6+$#+4^t0=gJ_w%)zt|Fh$X=mZCh)*O}!AXtKMk?30E^Hzat3r!jS>xa#$lmdZ zC}jZPbiW8e1k2LHS2HoW1<(xxSxa|J0((!U%%9a}`qsN7J9Jr`P+ycpbs$Az zI>#zcC6a{B{v>Sb+l1n-0j;5)Yb{mu>rSdIbU)Q7(;44$EQvvKnnchH?30Pr1dY#p z%9u{6^FcE2)V$ny8Bi*j@zU}>41@j7NKONvrrk=!OCI%n{q>JFteeN56`;Jg_Ayul z=s*Z~mS9~j79czI;bhT~y**%UW|l^nwnQb$(9}~6wKh4}u;>sQQ_g_-3=4a>tXzXC zScoCXL9P`eX}~#FMTl8lP;v*Xc7V^iWu--r+pdGp&USIF8*>TYS(H&qg^&x{JXnGM zM-4B`in-;AIckaaK5S8eEU5m0dXUDh4xYo&%ol28dw?a-GO|`Jg4^z+-!BeV9nm-r zaAos^0?#pU+yCI*`l*n4?uiIErM#j0Qwnas5N~ok!m-Gw`0jQy=%?9WKl~xs(B<2* zCct061noO>9&A}N(-6t|%AoUz4>=;%wu)eS4AqIukZCY6B~MoyuW`#vJlhx)c#*A_ z6-#zA0l0#^Xo2*L^7%H``FWkm(yM=NehKm|&=6{*K5NSmYNwFO`Pz!c_ggDFr3FJ~ z0#txVCvjL|gw!<*kz^^uJ!?Oi7>2D&Rk6Roy&S_|e?_{vkw+d39>FMdt zDNF9Fp69Tb0VNv=XW7(O>F|OPV$81w5)!|m0cx`k+KGv2d07n|UhZFLVb={&e$xaa zonM?B9v-88Em#sl&}YU`#Te~U3_~cxSTsQ@m!ZEOX-&ClMGwY~A&L`$ zlnTQFAIZgBu|~B@-puF=9%@I2#AfwprA}d5=THe^e5*x0aXLd$kB#1geqG}@v z#$l=Vr$w6Y=9R5~uXRD~t1#em-|Tn&f|fY8vkQ8$lrVcfLtx<@D&;tmsDG=%r+JE# zdX8*cwr9PM!YS*n+7EBv4p@_daEtCb>^+N&Qz%ealTam3r;Tqv8-k=8Y6SH=0k@Of z`+1<aji&;y7vtaUT-2r^kd#t4 z9FGTSWpmE6}+dDF5uo5nqQ7yt^GcZ*q}NO7D!ny zrEsCfwc6v?t4?Z@sbG$UVVSysR6qH3la*)tPQI9NX+TgT>7oH7g;qJrMA@G1Mw#T& z4ekM)ObeJEE62p)`szA0w+HHH(#>>QTlsP4JkR}ce@dyY1DSFU5I}ST_%X57jH>4_ zc>R?-$G&wU<9onc9q9=7)4z-)wMMvSN*(;~Kx56)BXbJtE&qDH9Xt?0nVKPuY3SLQ z!JWd!jDaq{eN%#7zJZWo`A;T^yrN*X5d-n~i5y5Th`~*(8kE6QfbF7dNAl05c8H)B zOa~;G??9>ZeE=s|FEvUp&p>?|q(M+0RyqVj&MPveUr)C_@axR5>~eG<|MXp|!xW7m zehg-g{%0u{EUVVm>0!_;TjpqaqCB$P1;?x1ONP#QF_Y#mHnw`pQx-+AH>UEcIm$Z) z8{T+b>u7s+I|6G}X1IezEI-BvgYiQtcPYCry{1laDL{AHsL+NIqZpoou0v^?gD-xv^O?dCI>M;Zg<*aYx?MHa|oj&nSnh##~@J*=ROjBN$kWXoef(L zBUyh4Uh$d3IU+fgm-Z@KMH256oWxBf#g7#{{At^mYjAd@2Yja$X`l#t7z0w?ed1f% z2^?5nG(bnXsdZ%h^(8M2qxP?tK(mL!-GX@eF7JH_GA0RwMK7S6-wMTL1TI!#kzn+P zEK-O#D88L#b-|W~B$u0)o0cBZ1FQ;Kmq|e=wW|h{Uc^^qBQSLsUiT**Er`)sU)rC` znxe+{s>RF6Yg&flUxdt#TzBTsiK+moYetjO)I&LNet5MC=(bIZ+snhvM}kyFd2kJY z=FY|FSJ$rk-@i}-2B?kdl+|((=gUnPg~j#2>RLt&gY%cqygiA(d3;fS5Ql*pp=2ac zJoennGDgpE`5vNV2ik4;wbuG`m!5xQYa%lRk^Lhtj>LZc`m!t2R+PpA zBPP0|)&+w)rNf`~9c3RmYLodJO%b35Kl?u54lVQt#Nn^#7T2u|z{e&HJ-eIMt9~`= zD1u)L-xTh65vL%-Qdcz2IL6492=G_&t88n(FGZ82F6U1;33tEh9T2mhryoJ@%RH;@ znl@kQz+0&+qQ5n)A=FC!$TUSK8}J6QvR~YWN+86T0Z9sYIyo7ZrNN5ahXGDv<#1nl zwNP=!-xmkqZEpSkF2`@nj#XIrE??Q{Wb=)_shhYU5BJYvQT=!w%@p{9A_SMz9Wqpp z*ZaoP5d~_%>|cvrW*n*k6%?GKsE>}!7Q#SO5iu!<%~;`y2m>DUHw_SEb6%<4_;fO1 zdj|ni-(V6cOIo1{%^U?(=gU$oDh)vF3GrHu)^9L}$ka&kw9aG3riAHT{?x0@#U`J$7up49y*>uw|d&R!i@o$cg4ktXU2u7T-C^FTB<=^_&(p7|=ho>!Dr#M;#eh^dY6(FvhFNZ>J) zeQEvSNfWp*y$ci<$20mXE^dxPpP)(A!)i%HI0_o|8&qo$JgX;VVsLI`_F(Vk{sk1W zNm;dPxP{U}%;F}CA}rzJ>y)iIil{K$i3h#3sL?y61+gR4AA9O=L}V`C>sS>g@CRq5 zu1NTxAxsJU324p%1Q5_m-Y!Si^e~kWV8RZ5Q}wQ;90HzBu2yklbxWVM@$+5q=TYJy5+OgN$PZG(8kA42M$A9Wid zhu8$|Xe)rj$ z)Bu1*`djDVcBdU4oz!6j+D_&McEMiyI(Gl#{!h93`WD*(p0w)*>K4ers%s@*Hh0dy z%H&~4sl?ybZ-xG&OkRGC3n-2S!3$o9JJRz{; z!l)1J#xi(IVf5-*|IJ-hM&EcjWCtL;Z(--H2z4bnwwuRp@_pcblHZ@}F*}bYOTU{G{PI}qY9g-bI_N;Pmp4QrH^=2&yVWNOFu<5`zp7W-e6%@V8X4$I^o7kl zUp>$`=R7ujj%|S2GTCZIo(CN1qGwi8Q9Mms=)s$y+-e{$Njabve#pMD%EwNf=WQ(d z?*oqHCfY>ueIy$c_d;kgktIDX<0=|r7!t#%*z(G z9S7c)-%6M1?)bFl+SYr%1=ga4%u07(hMH2bM>dIwojPi9vCjF0Oce><*ylyA=24*6 zTVi>lA~-2q+r)!t6#$Dr+AQOgUAN)2#FY77myl+-XXo*~Zy}iqe&?!i^p}BWTed|X zSFJ(#5&_#; zuz2v4y08Td29Q4Dir(VI+OBB~-r|T*=${1X#=VCx+>WR)KqBj;F+?~h7~ojKUiAP- z@SuZW1}FfGRLdCyFb&h=w@XAb5(L`oD1>czJNNgFzF9MX6<+^(l`a%4endL ziB2S+*X?$>b$-RSuD(=%($@uFx?-v5^cbK5PIXL&afGQUUt_DTrfeU&xz8qA#tW?l0}2;$$C2hrP_ z>Xp!$8?f&j*mt$2dLi`fZ{InXTOOv@!1n!lwS#V3O^K|v_&6)Wl5fOD}<_S zZlcYg_fhW#BgEjP0Xhs;&kKW6<^4`R#9G9SR%KVzWz}^8Qke+PL9)!!y4_D>WC?!| zZadgoM5_mGCCp+8zv-Wr)qh(5-G{^bpS}l{#Q-eDvbk!`D6DR{XbWpddSjAefR58L zogdaOl&XK4I!@U1e_J+fme;>M5CrmCo{+tW2!o8`Ep6OK*S=G1^TH#vN;rc7p zvh8Xn&3nS2&D6H-&h1LMDe|CEN7=3owOIA%dB$bzV4-z?s^;Y`d1oAZw5@gWCgwnw z*9NqAi&eKGedU~(H60*{9z;E+e*hj23P!5)qSoLk3?ETI0|rl^63*+Oz2k(-CgTvd z=5URr=vD8LVt&lu=E6*c1Ri%lF6N(ICUXL?(FR z(2*hGOp=3zn$0c9EuGA6?QdVFCipYCPZw9qV3pk7$G!*&84bZyj8!+dIgG@5qIoze-dI<7cdRF}+QT2h6an zed4Opyw7sk3?1SQ9wwYed@S<-`02X8QKjce@%`tJ-1kTmf{h2FH-xI z6nA?VxkGL+l4~1gkh>wA7M=$9xLyT?oLXSvEJ|#GUj-qaG6zvKDF0`GAH|(~E+E@Q zh|+mgG{hz0(a%L_-f3ijmS2OydzDPjSe?QH3BAd=w6^H{aO7JHO%wkQnpfNh>Vscg zHrx&5Rn%MMlSSO-?cc2Eje|8{x zh!rh3U6@*6>+kLav9y+cu-vr(yFI4!n(S$i-DCh#+rnzdS&u&?29KQLy<+v7Xm;(h8o1!NWGRt~>S{T8rfj;=FS6Fy^>Vv(^C*l}anP>>Xzi?aJ4rR` zb92S|8hfG1aBslhssGgN z()_X9`4lbUrZVi|47V`VENPtz-hu*;!U?1@O_W1c-BUZOf%__yEiH`^!QsY0sh6CjXC^m>|&YT08-4PVGLr)c+3sMeb~l}ONDO@k?r31>Br9Ao0!J zcl1IndTDw0ubX+@Hfi2}CF^s{vg;OL3Q~}FRmwEFKm2$uJ7&FzbS~D;Al8N$HFmJF z-NKHClB;%l*5ZqT#PU_Y$qb%#qcgWi#cTRZjyTtB9km#ck`My=318AZ{&?&Bh1tk3 z0~Z+l9um?U6-VXH0`prnLBmkN{^Z|r(Gw~!pCAptlkEcvpq0A5%wcV1G2+02AFIm> z6<0jQ5Clnx4uWQbj92E4`IZNG`$+^Hj{UYDRE9P;6-pl($4QjG2tLe`u<)HF1*Jz} z3m+oUJGcNi@6!Bf%VNTXOn`*8jhO<9cicK&I52Y(R8TJNv*@H`k{Awux~6)|pzW9n zHq9LoUh^F5gG{!7Pe#33*!n5;{V40lsfRXNM?5T)4&srgNQB6~9tWySY7x7ObUew8E^!SzbpqpW-@T-9u5%30T#+YaZgC@W`fk;+Np5MlV6N$(Oyl75H3!p}W=^QQpgNF-=08+`zl;N=5GQ=QpLGBY z^mHeZzL3fR36vw6m<8o_#j^%$glZ|4ofvAqhFwU1Z&%TFr!djod6Ot0rR@&U&Zlg0 zjxr1GJy6XIWAFIndpx~hk6iD zc$z0&Kxyh=hF$i>5_I|aRWOYNjAgzbpH!G)1Sxx+ehzvHW(i_zGKh0;%2n1|0t&?O z?=p!)tIM?*KE?{o$~*H;)3Z$bc0Y`}j~*1-Dbg)4*&;kix#un*OWXXMH)@!!SW=>} z9jPyiDOW;sI4{ka$jK`sB=0|tV-7&gFio`EXC4#|@(JJ+9}lmcy30`uW~2ynVkwTW zE(q{GXKBhI9dHNZI0ECC3k*Ag;$W@5QQn15C)c5fLUNyFvZ>3C-D-% z{Ea`=!F-D1E$B{$bQxGH4oZI6A6xm$`t^LNj3X~{feaIoo`Tzfg0RQ8#VS3Xfs|95 z>vSxrpT{ekPK8<+=PN88u*LM9b!EncsaB^P;hEK&T&@h{3E||Uh7r3OcMQ4;_~{R zJj5s-zTI)+ESC-8wUgnBOT04o1=5eLltUdCp91fYBZy(vDFL;RN9n z%{k7I?NIM|AC55;>~|l0r4=!>UKUj?(mvKeuv_hyiz}1Jqe<*mJCo@J+escT=YEoSPR%h@ zdZnJ_ZF}NcDYJ~kgET$ol4uELs?@~{<(Kc48WJ7D04#q2ACm4Pd)>9LZv2CHXE>vY zUO02b3=KeWuj_Ge*D`N{g5U6Vl{&Hw)chx^B{`ag?+x`@VVhrOl>4#C7aCC6Vkn*; zoj1ouj0?Fx?Be(Y=YBfc&lzRt?u5IrI5mnbxpy)PGEbATV5DkJ@+v5xOoSDD=R&?E zU_^<12H4Ma*3fS@Xx;BZ$P6nKFrpzJ-qmzR9k3(yp^vA=9*`QedOj`-rP9?Nz~dj5 zZ*dk_#*?eGW^hQy!lnq1L^-dVjfk%DMnl96qzV?orqR&hCo1L5NSL9;Nr(>fmZk}e zqZksC-tMg+D2+;$&*$ixc@o=DOJ$T-H?ZA801##uhGE-}JMCm}k#~{+;o43^m&Du* zEVq*l5F+8_rT}t<6S+^n0}_Q3nd#DB=%@!d#U8StE-OA``Rq8c^W)Pi+h!p%5I&Pw zKB~yw$G4?CFVY=3xxWV!=hJ3BEZFJpYK9@Y&+N+BPdj?ftJR~Ct^`{s7Ic(z_CMNf z0hZZgL0ca5+|^HYq%02|XIm1rI3yK$8>n`#SQEpXSV?32cPl^t0A73qXR9=sJCPrC zs&p#4_*e|VaSAf;JfmS^!RYco02xZv_c>5VHs|?E?kN+qp+kpx8mFwljK^n@Y^@#k zoX_)#7wgXG;@_O$mZ@pA*Rbd|w80yIznsozbR*SMnLmPu5JsrQ@hDAudhICXqv1vM z<2cU_Ex~jeS5cv#6IM{dI( zc9_TeTaX;DUIc~*f9s*%0${9@oFwPZ=k{ks_3n0dqq-H}`us3p_VANFkaZUjDhoH= z*xAB|Hxn>&>druOOmsrO{C~^w7E0v?kbWMf1(Sb$92ZCmg2auk;a{KMtEqa@GI{D< zU27@RWA{Ary01zt+VaA41AJv)$$3~}{nmTOZ3+&nVpi!@ib|J~_}$0r+FrfQ^AEwz zUY9eV!`0RE*_k*>Ekn9>HUsBW_2u3$m489h2hgIhwPJC%h=|moqNWBcXDt7!C-$la z-8R(Lb-8!hc_8o84{fxNT%1_lU|1_XzSC`;9qpgh1@t~$Wj7IPELlG-j4X_JmwCHX zqv!Yy-7DBL&#_!$c1-D@jzt1h;nyQQ=cIK1W>gbhOn)hEm;zg_woqoC*Q9KA9o`^04!n_Jh44S zMXxs}^WuL>eh7Sh;*MjpkXD{GQ=0@DY$0m-D=EPwhe`*ooDQGng9W^8b3J;v<~!i5 z+@<=o!&^8(sCF*;O1{Y3jduX)=KfxP!a48m^6SDo_f`4g*`i4z9lzG4)-1x2J$3P* zQ?pwHD8ZS20Mj$0*ez-s;oJCVO}2_h-EQ=CyvhXk6@&>W24TFB%qw-j@F!?-;R*ofQ<Eo(G{KAIr^V_(674};~Z~el}s>yY`k+~Mh?v`)frhc zMYAupti-EKJ8pl2l2=5HIG^228guDaq>Ln#SP9tDF{KMeu7<}|+L54zS8`A}UK=4_ zAEXT_J08a(OA{)9v~Y+;O0Ywy{jBD&mng;rH)cFartHA#Nv$#mqB!uiaes54O0`LC z#nzmHq<$a`=PD`%jLk16kdxtXJj&D-L6|ct#={rukyT?WuX&avV=J6ws{%g^pUR-p z2-+b@%7sZHrS$tuhsTPF01--`F9MRD^u>1HV`W{X#b#g(hxxj7>TpzAXSn+3Jxx78PNct?3~s(ht=K1*Wx(0gkHe5BaVMLao{6GEB+efQJ0P}-F@%i5!I*+V7=7*0o_!y@ z-Ad%&H)aLtQO2aokz{CXF0vE`)|P}Y6-igc=A|!?8>0j zT@-LSxF*Ln^V-Df&qZRbRD4u$8o=w&YTufS0Q(XEq(_nx$u>oKnD`u+gB~QdShJq} zLV_z#NGi+49*q!d#+fiPm0SX1(j2xi#eX%WWtpZcTVi$mEbQyA%VF z5)aWHYBXkb%(tYi5yKYh&x)Z%m3@Y)W#X!~6?1;xl=Gir|6d5MR7BOIjM~zz^kIr$`dFRw*2haiBaq;JV!u)jwt2W8`)rldx($$17XR|x zSsj%%olOv|D`6@uq6`ypeBFJUo!xeyQ;PZ(vw|*+F)m3bgWZzu^CxYZQAb#xaNio{ z4DOj5H0a^O-o5uZLtFcV1rgu@t8dDi=ELvVatK{RzZK{c)TyQhm3@qxljff;2%W^# zO+$);5}r7TWE!|9gJfFt@|JdF%JRnJfOD%}U5=~_p{p#!@}{=KRAkxdq>UNK-VCqJ zhP@4++Cgajm7h@0rppDOyUUi0 zZ4JtqhQUj3GIgaFPVeywm-rMxNVc?;f<8X!-L5t#xiI%gx|~l_3E~mImzGcZU+%;O zR^12{%=o(S6Sy^tJ6E&bxwxnZC4}&u)@LTa3sB~hm*|eHS%09$rlp3aA4`13iP@}! zg56jE{D?2Ivv!7X%xayd32;(zGZM&;{M=Ubrg=#l=Lhvg6^PZaJ6#a zJ7bLhYZh;4n}r>Puew}RL-dJos%$J3*s(mwS-MB5sC@2crcd)aPJf4B0>ko$JhHT| zH+!{NRkbI!XM?kpmeKh&ROF+lsH)t=^;H5oC8L3_Ovf)K9DD}gCTc$o)FkU^3^a@L zhxi4}RUsz)Ov4hBimpFg&dLaWSHLeC-e5*j-f$U^Z{QmT@gXB%erx>#5Sx|lNXU@NgcFB198ry6q zM{IhQ<2H~?q^-kn71aFCv~#1+d1aP|D<@##3>@(Pg*)4j2$sEiA-KMNsX>Sq9*p(p z4pw5k8RIf*DqSOrNAEnu7_=v#{W8r>b+xG>{Tokv$0&Qy*I1xzAGn*me|NPeZ0Brj z`TmT}RRDy+g_H)6kPIEq^j`NqjxY24|1Fhm-Y2Jfg)a){)?PZG;R3kK!i`ki(mkQW zjsJLIQwLf+%&(twN5Uv@CP(KIIg2BtNhcvTHN!CQclly49W`R%kJr-7jPNBYd3~2burRw}WmTuM)yr0oWW50#$mRga1=xH3=2=gePUMZs zkCNcGXA;iRLAr6{z;zm;xj&!&ht`{V<;&P}j1JYiDX~$<>LpDOnQvmyt2$zcM&C|M zrF(7DR!dB!52P0amo>v!p$Ae;{XvXQF2CEqr4xzUd~*ljI>F==YvjniyYI|VH zOPbY|8DwLHj$!Df7R7yn%dvd90+tGMg)ru5a@!2aB7)&R3z->LzBarTIxej(wm=P$ zb?696^a14q7zCj5S$GoyYh*Y|zl4$gl+rOWD}Da3ha|V{YX~AX28%fNdS@O+&d1ce z&_GE?NJ)VF*6`D`wp*3=4UX>wS==8)M6RjW!T-pP0NSNKHqw!Dp8pHQj{m1B@K~Fh zTSII6wr0t_G*w;LVDLZox8jkwlLzDZWMOOIp3opUUszrG$2}~fIJsq-oUr<=IeoKI zTT9`ZhIoX7UV;76iBSc4QlqtOAEb)5ywi_iGO=cxE!^uB?L6E~vDw5rSjhpZxFxIC z#pMo+!u0`XNZ@BDNH)tN$C~A3-S(Am54hHnuZnSmb1^PVLdDdzzH-`b*C>MWI7XL1CkR$ zvI0~$bV?UUTjgDGfuFicN9sP8V39FV($lVcs9E7&H?0=$^rkxo%x~JymZ*1|)+YEB0+L&m{ZqaIXgPHz)#zW7d{!-O0rN?cRtKzMF@%AaQ*a*e zYO}$i_efXO&kn1rIHr^sG6AiXbE(~CZ{TNBSH+aC{P`+yTCotU%7MOz{;~qwVMF$Q z>JKIUOA?pZbxtj;ou0NspW>*H?E{sGKx>e_>;B1$yONm*wUsU=-i+^OcbYh{`>?VL z{1U=7zbae;G+X=_p~lQKu2~ZhZA|80B14@Rwz56bJZ-_rj7f*8%mI&s zcfam^ZBD>VvZ)XE;rMAWwa*Cl>z@e{a#?no@JBUQSWSpV6fqK7H|T@2%}IT&xw-)O zn@`_g?=5!Copv_9`M}yQ6%!pISVSWy3p0!zY4s~)C1Za+IOmeMU}trv@xhmc^5>j5Qt-I2 zaL~=ys$D;H#x0fSK;+610A8;uV*?W3LeWw#rTpk?zS~EuN%A;%&WzlhZP6+~7a5_K zKoyQ3EbLv~ryWmVD?qC@$D7lt+S8+Glrh4lR6g2%HWNl43Mw|Fzqpc=2tY??d5<6Y z4qTLR`YHrELS#uu~- zg*1qHJ3ZUjQ1na_%z*6Dxn9H$)K_&sIiY66c=RDbFEe^@K!Pgx)Yu2{v+hx-Isej| zkTz3m-yUH%$rqpuy0C1{9&OAlOst$oWxrLyr%y}DsO?}uWZQ&IRqzkuIWOs*w9g*5 zf0kw#FPg*dJA9%LZd-}IZ3g*c}Q8fWMiuzHNnQ) zV97pK^uTM8Up@vFED3zHthWa8{q%|Y{j#ya#U7EKkPQ&PQUIC@OswW35&xrelb1I+ zYvG!8A?d0xjlqXfUap&9@Jw6FP}D-4`96zI9qT>P1w^(`lmo(wcw z@->s-nDci2H73hdmq`QO@JXz02l0QSw-hdzvnN)ZiNE6NtMTu)l|k;-^D;;PG{!L< zQI38#(AdCKniAx2jM#ZSiK%GGI@%QO zm0A(5D+8F>NC)}=RMA5*Fid;xwgok86^4;IQh1qx@thHrmB;p&GX6jwSjSVLD3OC{H<*O^pK3of{?jQmj%0=15&$* zqGTXb91t{75M}z?vCzD5H=j&C`?gDlO8VgHo_#ZDD5l8s0wbjxaW)^?AkrJp(cPzU z?lAog1C=P_jqc>85HXb5`qmtcR5`ds)N3U0GlHd(v!z?tA#-raJQJdELchEUP;}Uq zrAP)*xl{%qiZ04Bn=hn%3+KyGQqY&d2p1&EWxwR+%c?W){p9Z0g88j8kx$4xsq8v_Sznv#sr#nqme_y!JCx{Cz%P6^os5;%pyG&s?xH~MU5Y1widoMs|{AQ2urUW zQdu|ZL+Z8hb`519oXh`8v|k&pMACe)Rp70!-)7l}6%X58MDuclE4&i!3b=UQr#Acv*69O0>eL_V>BK-<&*5qPHV1#1=36mh^%3sW1@Z)Ax&U8H ziaBfMx9{_dp;ExkQj`IGj2#LRN4yX%$rT_y_NYfak?eLByhu= zLM%M!z^D(hLnS{Q(Zt6}G9mxz;@`He&p*Dk-8tcxLG+y1Ed9hBE}82_0(_jsFI1r| z$^KHcmFe;kkAa}u3@4vNyKpsVTnhg)@8AK`A-jA+{`V*qk=hm*I3<0s+QmWog_Y7# ziN=_PPt>Kb21z4cuKEd@7<^t|pUxybxXR|nequ=C)Y4SJo4EtFc+a;G#>iX!=n;i* zo`0COx`Qr8?R)Z^X{bUtyZ~z-Yhpv}=N2UG5lZ*I^c&~c8x5G!oc_@zIL-kg$~f35 z7Cnl3C4cK#5xxXSJUhsSNLa1_2G19D^Zmh{NkNI$qDx)fSMvRzdHDImj+tcAEdajO z95F0phNAs;ChVx&+`O@D5Qq=xjRom^>-2y z!CBxc1<0gFh>doSo)>h!RkflVl~gpY<*lm91?Y`vYzJPO=QAgHW2jE6iwgy-)IRsJ zIxcWkt}Wm^R+STn0lAVv?-mDOm6O2d8%?Dzjc=cYjx{he`SNU$Mo zh$uI}m3(DYQO#1`Z|=x~C?i>WnAN z{O2KKJ70NLg}#6we|m>bg0vRN>mE9X@wCHlaSk`az4lnaPUz>#qRc(7N~gRB{rh5e zk7pnq6=-tbU$h5Ecx)MZ6!7gUe+j7=3F;I7nb(s3<5EpAU;#~OM`U#5)tuTdhjCdx zpb-_>Eja_lZLWxukcc`AYC{oVU3G`Jly7<@SLal`G6`-h=zhAG0f&eA$J;t!`Y!?lUzB zb1w0EQ1GV2Fz1OcJJu}ayoHr7xoy>$7@fd%Krzmdj)+sX>^oj&X*@!Y-1u2mb}^$^?+EX>Y409$Lw5e2Xo3- zb87vr_ls~rl=wW?Fs~XLklnHOEjD7_HcOjFskK9r<$2ufiGR*kkBV`}!p(~r5@+fP^K1Le$4T6E9c$f@EO6dORmc`6~OXfUd(u;hEG0A7arh@4Jy14lma}L z38ioe))AQb(*bS;ujC1=lfBu`Y>Gr_zqH;?IDT=Udj%err|7)fyuFkGg5M%&RNwQw zUzLU4(?7k=?9=;<2J0)mBO%j8VEqIHr!uCb9A=5+w^-gzN?NFVKFGp+aobd63FFWU zeH$KAInjACXTopR$N|JXI2t(|=4!kumwT89VBX(=6tO$FB6JgD1%SoqsUP`u#s)|1 zAS=*fH@A6SFopD&WG{NN1@e=*;!`2jxBVTPj}@K~w^G~)0RxskUj z;M$!QbRJ~mB4mA?Ss1GrdH{W6|L>*01o!FRl&-OB?`-Q{tuiJy#HSE1bAwPi!lgq$IYg#9|&AZThu zz_95ryyU4jxfy-w1oh)yG*ccdU4A*xWCgo|n@nQ(w1ExJoLucr@s1+i(W@VsfY*v0 z)&V|_IbLZduU#Yr2c|(s;gZ`cO&Z4|4)q_T#ZCvzdBFsgqy<99SN7v#aE4t#$lt=K zWq%|1EZV@3@pX~H=ivLeE)Qo5)ujVX&@N({@PG|Azvy7}x$$!YvR8>^3sj)H&|$MQ6FgA{Ortjg;Croc|D#$K#9Cp(q;Ch<1BRGL}` zrh0u)TOy?;IKpr&m2EYVRf5liQB$cok6E%e7$7`1_F`F-nyZ^_B{IkM2wjwlIRREu zz=K$ns=%IOl{(?#-#RR&${OB8I*p|2*mH0&+uDJps_eCsZZBODHLoHNp#*?!DFCcp zhg87p&d};!M|sV&6CP_@Qy!(hy~B9xvieqV>$eoh_xRXm#ajAS6!RJ$q;gXwH?vH! z*W0;*{&s>sNDUf?Xfc~6%C;WC^aK4P%oejNn;h$o7`9nPpf*qOe=gMPY;y$iH%_Bq4gS1b3;j#$C>$>P0Ow4cY_}$wVV8NZqiNI3;loq zbx(x7a96&x#2z7*Sy$SL$}(iTnqu0CCiCq!l|mNFp<#^Gt#wb`oW&XI$eO>Ns`SLMccNjhLDExSBndPG9`l_f zY%9(?bG74cZ@wd1{YJZOePQ9YWBN+zSa-FL^R7@P#l|*%6&2no8*ph&fnSL_^8Qb_ z_(fPf#_4>T;Svk{wpQf*(Ejk+dX9JD2{^X7F!9tP@^0^;DYD+cU}YYI0D7slzodNl1fxc80~q@)`XsVK z{&I*|vFC#EA5n==#2xWkpd(#g^VSKY9%yO^QJGe*11!pyvP12YfRC4y$Ow?*lUV`Kz9MnA#GOJm z`GGHXH)OYNRFyq9{ha2q;ypqw;xXm#X+R`8DioRM)AzP6=t%{NhDwWkdfaielWVCr|l#R-^ZhV zfKtc2aOSmG<~3pA4d6_I)MJu2f-~yP_@;p-_~uY-!$Jkm8)NIs2}s`v>7+g}q-@1a zUT&Sm9yro)Y&yPevc3kV6W*yrG?)}+=uuqdQh&%9F&3MmHcu#JOM%OxxR`aPs4WyH z85fZkb`Se4Mf)+2{Nlu%y+*R82W;}veS*}D+?Q4A%A{YI8UR1tYrGdqNQIVM;95PZ zzZ|b6zZDWVjTM4k<@mWOJ|8eJhU{8GOLMq7>?YVTi80QKz(=y%HNh0>Pn1f%PcP^e z=WAJ@LgpS$gQQZI0q=n!xXtE+?a$wx`Q=X3$vo~r)8;>cihzGg#h>~sTc5INFs;fm z`8CA?S#ZzBCkgEb)o^;!lQGmk_w zAl?6B3J8Zc#O*qSZ%TwZR1D22sG>=)e00Nx;DtUmynP5r=uc0jHD@U-l!BxC!3`-V zGGlWUmoku^$F&FbJ$gVT_M6I&%8Y^e_&hv$uzejca}Tu3Rj|SV z)lBL14>NhkCg|qb!}4@Lo0PDixvy5?%~KN!MSsukN|=72#!Q`k*!DW^iC`n*OH;}H z;Rut0Im9gj_$umI@WK2$bkdVE88}KWv+7Vfv~b}$s9&L^x0|X z3+aY9KdcDsaEEe5)nTltxKd3;gmbUOazpR__KGfldBxzYpxVOH64hTma`Ea01D=4D zSrwL;(M0F;Ksj|gVqQ4RVo}W9D$El2&t(NQN}HBcS%Hau;!ARORr{`0&t)oANY2uy zvR!c?vUtz)7g;=zEhhU1SzPPomFQ9slQkFC6eYiU@%<2Cv2KYeoPkMM;fZOHESj*R zbKfh8X{aTOqqtejYmRdXxRFc|czjX#dV4L+;yZoIZ6ym&K{7UsvHzO(>_#7YAU zZI8-la`_$S@s8n%s~6$qwz24A*{KpInE}W{h0W1FCx<>cRPEo7Y2N~HH^%rqe@&}G z)4iv)`2s?#jUw|&!?Cmvku+L7_7}SoH3uF2=@8xWu7vZcrmHXtC^~TQS@R3VJXn!YS^DBK)6+%R&0EhI<%!MbrY5IWHVvYYsfLi)5c`RQ&Q2IQiK zx$Y9OOFNvEktwZV9z(?i^|tKA0(@6qauUNPuYkA%BX(tsVRq;x*7n~zxEWeHn9ecc ziW-W&(nn$)a8M++3E^Ym?ARQZ~pujtb9ao>#YCB>1)@Nt-az5x|j zQ--eM)t4yNpjS=$WEhnV;O$x1H-)bfLzD$|j^y(iC9<+j6}X!YndM?D`sh-d339FZ zHUKHv_!bzc;W7;SnFSV0Hp|f8Z$x2@qJcKVm&yN{@?rcs6-dNRzzS~o^&MeM!{?ew zw%hAb<10o1C~(Q(bO@n6(Pdcil)+kpW2uy18rFN0Y`~o`CJ7wR)cLtKBMGMIg)f@w zJG|Y)%#BTu;lu%+q7t|o5Dwo5Ao@Th!UEb`k}1C}kLUF8ge{gMky6gGY%y}t4~l|9 zlNO`7{g90nn)6vMMel?+gc4aPO_!@TD|$oB#lT`O!xBC;uB_+8=_&BKje@|+pwu3N zQn8aQFx;Pqvva~)Md-T=PSInsCWZ7DxyPGr$pppmnJko_f!M)64`Hw<06h8>TEa}` zo3U4tY_}dh>jd&j#!AnHYfV#~pD_(;JM-7e?qM2RufiU|MJ-;!KIM(StJiJ(tP#-S9mk%1EM$ZGY=#I;7T6VJM9Ns!2FLNB3te`zaEh`qNPGw zix)^R8juU)n=X5;6hwLz(6UNCr8AXwUlg8oL79+rkXGY5#%9?&jdzy3Cwa?>i~f4= zeplD+@8-3xp7&}hj(rDxPkk8cKGY1H0&8zaZ;qeP@e7v*8q;59^{asGJ5gfQ&8{tr z0Ie18CaZ;IYids^pa8Vg+c-0#oWNO%9}PSr8-YHcsd!Ld@6vVjBB;L6QMuFhX5#)x zn^C6op5yFIz_y0}w9;BA@X&P(2BTy`<6ePMhn`yf>27st03q$KzHE%6Y7a#;U9miv#X+;W0KK;MO$9mfje5y?l^6@c~7sSaE4 z^-T4}7Q4XYkwBY3*(BnnbQ%nx$Jm;oJ_ilT_kr@EhDeSwqGa99mci zV4n$^>jVH)?$5hQwQ9A2axE`5?RHX}>Al6t1e06qv@lLp0q`Tg2l2*bPCPDHKXtmO z?e>Rr4oIloo_Ap;K&!ZkM)RJQqM_1$q)8Oqk+>gGvCix=5G!j&V1M)z*SqdZ5cyEK zw^^9_t(=-}CD^OR@xwyjP6u#EZq;qRLy&M4=Fk&x=*4{~EVcvOJv%z;@Y?8iwAHzV zzJ_`x6_p~JuAy1CVE>|x8?PXaCrs$e1#PO1MG~so!Os@nu(%T zgKfG22)!kwV(1hMB(c#BcQxDjGf)ae#D<$Nz0!T}&%T=HE$<6eIHO2mz=)h*x(+?Aj>LA7ohs`N;SgxaW(N3UB|WoiO5qwPhp6+ zCD>TF1(D>>$594RdM~P-W%Z6_P8l{e;JZ~X6n!$;y%MskwrL_2vVuhMMEj&B^rQ8wKY=zAq*!qc zfXo#K#)?prI0o6JE^Qcz;81qhgf-GN214_CDAW~|Uq6_*uQdjTmY!MOPW+6o_BWqs zE2FQ$$;aen#`ABd>Zj+oSw-;vzaAEam@qMAJNb*1npGs20*Hl~SU)B!oN1+|RO2e5 z$tn$^_H!Q)bmP%6k|jfnz!lU;UaI*2>Ss_TOf?Z_N}x5h za(=-$hw(_scT zK1iW$|9wXy>?!qIE9h7!7gvyYT$mB0!f?|Lb|qZ%TNkJZCjqPa~F1wf&g5fv{+5M1kl8fzpl9Al68x zv=bbQ!~w7^wzUqTaD+qbGBEhVrSRyRJl7uGuMS?@*LJu}@@+bq07 zLw7H;*4fH{|C>?#?-RwbVXQtB_9i`1j)H59Gbxw#;8}QFV>~Fl8`ieLQ9@9IgRq-S zGIJEVWXk>3(gojQcAkYQ@*e(*N>Q-_U@SOTK^QS%#LxXO72_IDRYX0THVS9Bwtfjz zDU9-Jy5z_n1HG1|nJHRZ&<4oQ1AIu&6ta86r|zK9I0!b80#6@d)sGticE=~NU_B+Dq&KzB2;s$sH2sF{ z{&HlZ%En|7qqAHWeGM3f=!bSfwJg?pPgbNGWvM_>xpvr8@0=UXTKvUNKk$cS_3lT8 zP6p>pcN$y(&u&kl(!b|`p4gDWfDj2+!OvgsdJ@kY3$t134*vY)Gm;brPQ;l+yz})m z3ufYI#TNYx&REt7~m1UfcJoqfnOEqX7;c$zI^S|y3p_B{8xijWPfVD8~)hE z3Gv#}H4Q4~GTQV}vqkja2X`XRSM>CpkFrKLP36q$d*By7Pwi1~ZkDJ3`$##W+NE`^ z2&=89;q!Ct&EygP$_~0gieQUGTrX8C*|vLUA#s|^3C+12xGzzoqQnE<>UdT<9d?jI zpm&UrEUm5O>yMR<3;DoAQm~W}qr?u2fc4*@1y@FiBhV>~z04rkD1Q`lu^WE5nd}Pp zqm_?mh*`J=6OZWQDR?wWb(pbUnswo})9GCmt{Bi$43ZhuP)55xKQXa9x3hEVS3?UB z78g>=oKI~-;M@nWipBW6$>OB=YZ86Q_>3vpn2!lM;BqL}rvioU568}q%ji5IwxbUp zZ=dO(Uq0(qZJpQSUHW7)zDqb{F@9*Wwa|f7*T9tQeg}F94BjLD=SW63zNQhq;mB0F zHUhtiLux4apR+}~sJW;SKJ)vgX0)HxVW~40;or5$^fg}zjpMx9h)AaC3MGa5x23#g zrb5HGZdk-7U&A{D{#;o5CyQOG+<3FH(fzgXynzeEmVo>!4O9oDRvPiHS_^t+v%<;s zBghRSsgqIlQh?EpJswus zh`72NC;5xGwRvY=>Fo6F{aMZKOgWDpu{=!3*9=w+xxkbspO;CdgLu$naq2;H5@o?fcsI@%6qLpyX z5;SUt>xf<5rfW=2lD+KlGeGw9h$&)c^2R!OI~e273Q+%>MJ-kqSS5IK#pl+c0X{6G zFg=OxKh3sDe#`-mmVkhr0Fi&Ila$>j1}}QY4n?`Wv^>Zd^3z1k7Can->(6<|WpiEL zyt29>fc%ottx6Eto8e0(@2I# zutr~Wg60JUS=&Zfj-#DTQXmE)q(vCzEvLIfivp+fqCeO}V}Hcx2#jl##euZjYlVsg z9{?FgqyZ04!V{An&qB&GMa?zt7*k;ywZy?`42HSIIGE0G%6k^>{|&T)Y>} z03<=eR^ST(kNP`RrozGhp+Sm5ve<8S{dkU%Bj~4|XsN40D`X(8q11L;Hv4(+d$aOj z2=_irdG}U&s;mYcQ?t_-4}dJ* z^^V|FGsfA~RBb&Duz4C93o;ypy8?`CireShEMGM2oKuCEd25mC)76;6Qx0{dy3}yO z6H|>f_=3i(^bN=gOU&}I5V{ox_z%?-aAx?`;KvQ1X@d~EaGtAnFH~$IT>9i~8;<{D zg;ZfgP#(SJmm6z8LVOg}_Ytv71)d`;Ftup0WXkn=auo`eh5zLTTRgcIb24^vH=HUF z(;bk3C^C>A02UsBSq*KH{P+N%m9i*Ads%q8<1hUHpw;KdMnpj!sU$-Ch>dF1xis z4hhMFeqbI-R1JSe#!Q6#9O{_L9k5uEY8%XlxBv;c6f~}#nn=Qv0$>Xuh0^8x)rwGJ zJVWa_iWIErL?zk@nr#qfEE~tnx!kBoehdNk#x?!pLuv{SR%hf#&0{I`kYou}WT>Ml z``D(xHKF<@)4w#~?)`+vESIgWUPA?c7sBiZ5YbU4mPRF!t0+~mp66``oEhB?n}Ji$ z?mM8_mHeD0%?W-LQN)L?85-#3*7|Tb`}+0MN^3}=5O#LBSkW|}kEZRl#p~PmQP`4f zI@c@ue1zzJ@QB-8vTOE@@qT(5h-N1CNN7EhLklKykv)JC_ZfyK{A2+Ur`#k$kSijV z88PTq?YZ;(?JXLmq9Y_5P&Gm$i_2ppY6#=kXIfW|_`hPGXEgsI_GtnolNd!+06sW9 zS7-@xgcJ$HUk{NL{e6{g3S|%?UDn-0Cc44?toy)hjL&T=U3!LhzEMN?Ht+o-_PHdl z7t1)ErlZ-f7yZ4-N=+Vghd`}w4{u`wi5 zK6+AFFm0gV5=NG;XEg0mP5WQjCtWP9RK*-hf<0LvW@MtTLG+`&?k5#r5gDPX$_N8H zLhX=JC+FkcSzZt(^ao)=g#_Mz!-R_B4DhSQc<7(s3fDZ^95HUU7f59xj(tuHq_zc4(6DmMVY2$B&x zGcC6Lr3wGvm~fv*(y({rP!) zZcCr*I!Hor9TynvmXWcR;FpLU7oWJU%Tiv&|i*DJ2+mp~BCW-QoUZ?oqy-u0FLfM0# zz!P~$OaQHVvtyA=Vv)>$Mg@z`Z;oR5I$%^>AwXQDI%*Ug-2r9f7~hW|R=xCi zBPw)-Rdo`Bh7_l0B%XIYek*A)UyAp&9U3gcyK7nt2jIh|-k5dwq7PhAfaH^yJ3s5m z{{q-OFoPUZ75^WBKmLjgLB?~YmYNPnNT8k~sBj9%Y&|~q?0hHEU1$InD?*9^?kK)S ze{TG|aA&t-0h%Bi#mSIVD@QTI%bDQHmsW%(_D8<2BkZ>L1;pYCqId-8RPY}ud0%py(=N=)WF|gWggWdnV@+RP)>CJW2m|&+~#P|2R ziAruoJh;vCS1its#(1HJ`^CGXx{S2-rNi{ocBasSUm}+(C$|Ex(2AvokljnCb24sFEMYG=+{FSSTf!8j^4XrKGj$D~N$8LTNy zSt;wHv;a(ThhXyy6b)sh7RU{&f?Ny0wZV{!vm;@n(O*}%^=AonB{W_spVH&LE?aZd z;XMcAq)a}`Fc8Rml`_u#4Nk1fYyuaiUY=JBYDI)pzhJ#wc|}uH4WKWk%Z@vGJZ_SzN*NHCNo0bD*~C~` zS}h&2E_;7`q9U)vyfHd{5DM&XFAO!HFq8uHSn(LXBI>Nb4fVgy+>?Z_RjhTsuBJL8 zaL>RCFj?03{ea%aG(~pj{2WWK=$PI4F%*lpmP@Dn4J?}*aS#;)+Yfk%oaU%gV2g%q zon%|X8v&;HZG61B9Ai|ZC26+lqfjY*BQ|>6xqn&u14>BF8jW6s00OPRPmZg011=}P z9?Gi9K@hscs5UXqfvL-|JW(VlcVQywQn{+l9M{->*@7cNHFf~E0#73!d)fuyO2axF||xVtZkWgmBbKp6OW zisZ8tE3b^!qxm#QzG_2eTM>+SNT_)LG=x9CGrrOhKRBl8o_-4TXW)npZz3x`?vQ&u z(bCq6E|UwF{$O?;Zlw`eZjHeCKp@w<{H2U6Pi^6Z;>w*$O9Wn zsDW&er9?&~>QfzVrVMlmgQW8(nY^M(SgU}(GI~hp+7x*~rZ46WWMWv81z{;&0A*TE z@y2pAqYs!D@eX8nZEzpp2wU5@1>mm6X%T|Guq#LkA-mhn)2?fFnp!^UVxqH22^oW@ zLVerI=zxQ%N0^2vrgzDM#zl;-!!8TcV&pUFY$u9$%^n&&+FqVQGC2&lBAoJzpB znvz=AFq^~7kC3K>;trOeS;vhpejB;oX`<**kr~27ZT8LVdtVi;foRWn=^m4_qfI#R z3tBnqU4H9#Lz9SK8wz2eNd=Kldf?9&Y@MWj7s2CY{T5CCx~PsYNF6VkmO&_v=ZAKZX`#F_+H|&YE-zGY)X9_oC!kgPd2Rv(7CS{4Y-gTs z6bl@YoxCyC!4h~+BM(sqE!k)+dGL5%s!Sn4*VL*lEx>1atMz`K-O!-kH<+&JFY=}X_9K(ZYwik@udx8#eE z^H@98ZTi{a^ZaDk!KR^pyf(YU6|ipI^>|;prT=zcy=ILN9CCZ&Pq9UTVPyXuq}y@ZY{Xto2phzH(r0_;jwI z6WX`}@BXOQ*4!T*_P@XFp9fWXnpGUA;`AAGd3*3!A$%3@HCmfIo_F4^pT&8fsV0nC zc{ew6!bPI!NT0tQsaiigz^MwkKU$J}=d-;$s^!3nn)-FsfcYlAN6*<&pud6iGG&Ie z|6IiLys?h2m-z(rao!M_Y`53hYn{fQdVDnj8ZEY3-Y#?u!)~<$5bixI->sdlteaL2 z1aJ@|gU>6c&mw|{-8gx-ZtR)sMPLu1`}1_{PTSw##JjGlIjo%iR*nXAqji$1*vWSt z2~+~beH`n@S7iIBHE;!Z3cI3{OwaE;SbrtZFnDM$%XV$K(UvX_yf3AZk4*xt_`4n_{#8(v*pZ~9am63zDt7aS)xtX92B{Ef@DYBz8Zc`6=k z=$ZMuY_(s9;!X41EjnGIlxJ;&1o*M&-?v$FjQut^uvUwV;tfLa?Z38Uo1+#jNY^sL zTn`AlI!;^b;##e|43se6(0h%_4>E`Y9gmws-PqCjQNoVY@ z=hAY~0#Qo@o+Y0dVPffZY`2qVucY;dzh7>_Bx~O9RL%iB>z(ACj4}!_rQdmh@&1m5 zU5xQAU;7({+FZS7tXFAQ(6^vrWd+;4@hi~l?D6a0`-OAl(Gj{>0U#-Gb3SX%vMDap z^{KCNLmh=4nJmwYa_K+g4fyuCV2>QR_>8V9Z)C`SvtkdLpy=%}y%4hu)BV=QURb+O zO>OUjr!BTaNT)ch-PL%A{P~~?Wato7DCFHHF~?h45gMxjWO0s}jovr!r;4A=$vU!D z%TVNsfbZzs^KCCkUhmEG24?DQpXA${Y138>n;#rn=Bf}i|CPa^q*r~ndFr@;r`DyQ z=tKARYyncdV3`Lsxy&A?a$e0HTJMj-)ieG3B%yw z?^FxkSq6$*U6HRLA&8ylzQupOg!@YrKz2yhDv&?-k^(Xctl5 z%HL3A`_QwV_Pt5`PS+ok|NBQGf+rP5QEOJ{W9-{ZB3;pV{(N^|51M{|Jpm~sS;b<({AZNd}6cgc;)QSP3XD>c)W45qAc{(K8;(hsaMAY$nHSvgP0UMeQKPwuUx$h@#1l-16E)9z~XYWl(mn?FOg2A7vZK*9Fz9( zicS^hGSCR*YA*9einTP(QxRmrl*b#z8D!>~fn;IS%0bLaHpvy;d;TO`*qvX$^l){& z-)mnuuqHU2(5CPLE-z7F&r^Ab-OZ1*izU~}(N;L;njRY;Y6Z`itY^DiUWgcE;l%^4 zzwm)y)FbmMrW6GwP=wj?4E>Y0?^QyHd+BYHE-x=r_y5in@EQ1Dr>xI{Ftf=w1Q)p- z?|^=$cKngryan;`jwY~NxnqlJ-FE*U_s?8>+JatO*soPA-D)+8mXL2M<$ImE14BzI zL&IdqPQIM{*VzPHz{jJ6uHM3uw*IagXa=OAZQV68#t z(iQE?=I|L<$T+yt!lF-j0)(!FqSovj|OXAlf$W)WSgc?3VtE_am!TXOQ})J ztXZrjo;Z)2ivdGZkW&h`Lc7qfw){~X>FW8E20;}nNY|d=#HnE5+>{hFX@TC!pFs`}d(5;QM9-o-yw`dVtHH8){m7CYc*W9>t~~{FBUTmBYyrY+2kCtF?U`^`NUkf-@@(`h2vm71 z)Hqj7BD-M&RJ8|g&CLMQpcp_u=Fa^m3V5h24$-Lywvbi*uVm1Ey(|P=`44?#36?jC zS^1Th0EnPS)c8rDkju&tq_@H{jJ?HLVHYxzmwhxT8fF^HFhGKx(4?QfnhnBVm}ENw zu^4 zP*l4R{!I8&vN1NfACh%K`&Y8WYa$jS@Q36d)%vKpaBFC-KPK4eKLWle6^`+?Dqt@Y zTG@E0y*``h0nXq3oLR2_!yDY}xlrF%=r@GysDH6HB#EN@Tiuy%&IyjjI=Ne`fla0n zAyPgV7RDso0IRTt1w;m$7U&SKLj+t4{O(Nd4by61ldx;yuH(D+m^j_d|eK=-DG`vOWK=f88PqUp?fBz>vGg^i~1M$4iKp+O&P>X*w zv|zr0lzn{ne0~?_>uR}@-a)k#;$r~LhV27R)?(Q<82>EY{kz-zpUtKthsf&K6fzmm z2MFD?cBmu1XI~4m986*FQesC07@`%pxp(nsTF6_vDYzFX+|Ki>tH-bSspuNWfFS-p z`-bs4_R{qpt`TiuyLhnUROeUq*Ao*zNW8IHHt>!jc-z~4`lxAAj$9N2x(dLc6JLQJSuqVZ zMPNT=F^gAd>2PAqnm}=S9Bbr=;FPpWwvPo?8i&hsWY)Wcs67PEV$v(ub&;nc*%(*Ya4b-7&s=7S8n?RW(y%_YK zkMf5DOGUWHNtMjgwC?g#A(&y|NoPH6No0FVDxsDw;vFh8@rKexBufPxHHj&HiC}T> z?oCE?PL_P13HM3{_v zof*x-#-8%^*aAk`x?GqcJl1J%6cvvg26BG_Pa~UWwxXA{LVNSwN`jwcX=Rpd@I0ruqhEB<0BKo<%< zKYXo5D;A=T1Xqw0je3!!ViBDzhY zK16nmXDv^EEqqyFB(cbUSO<*X5;|LEP^X&wW<+_00xhq(EAAlM?>v+U(u-Q)Lir$D zxccTn(#!NfDfop79Jq_Q<3quVEJFSB7Q$~nRuJXZ0!^?E;W=N$gx*&v0J@wNHxj}? zskB+$`G>2Q`CyWdy(ww%Q(U|+SLY#3U*0%Tc!JrOqF~E8eAA8wA~PB2PiF4N1G&R= z%!Hz7STces3fe#WN=F%{2=E1?(lxN5*v))VGLUR9!odc=27r&dlAIpIsoXkJn={-F zRAAq9b<#4qN6p7+%EYeb!FVE*fr_R^Vq8esPN8&?&I@8SBom=?g@(waibu7D__!5xOaXxyS0kAl=-@ z^ZELR#R2I0H5oyI_WD)4zq-SmfC!72Of*Eabd;9x^l%+9aWRHjEYuBFGo`qyD*6$f z)Lc_tfs07x@zHJF0tSxB9d{ zke}u@V?sD@$9ZRdCxK-(Djd3$Wb!@?veq}4Q)1xB^=-~vJHqM0_EY9?1ov9EGRX+F z<-#7d#Vi_wVX~)cvK*J-wP?bhaiVVJcRc&D%u-sa{nQ(T%%-h^60vxE#@pjTsE^jA zQ(r33QYtUz76*kTviGOoKSceN8O3@%J_g_jQZp>?TvQOTGZRY>Gv2h%Hc(bvOGTT` zga@9Q-Yf22(me%+8}utbgCN!^ZDR7Cq z{bLN_@%e1U5typi!(=d8#J^=`^+W|n$P~s=&g;4Mid5W={?ISC}K0eqsETUmWZMi*TfJN)>IGP*JZ0g}?r=1u5Gn@v^N2*A_B5M9!!#Nr-dP} z4a|EzxzH7YXWB`60EXbCPUb-%^|#LHIb*@UdLSKW)m+f&Fu&XX+fY?=0i)O{u}Fu^ z&rbn63_TJsGhz%f=JlwM>NQnWm**0U?}_Ap^q1CDTT5Z9L$(0MD`yu9%9x(C@J$s` znVEVi-twYZGmG-|(UqIhAmf_Y3d=hC?%|n#)wzCLlVyk8jkjWXR!O&aM@4|7k?KK$ z|82(F@C0@~5|MGdnp_IWJqDU-er z)6$@VA8VX4$g8f1aS_oYXtAT(ezaV$f_CqgbyNCYQ$Y}uxVTp4yj}Z&^Zq0Y>aWfr z(VYumYhHgv7^R%(yjx>0F zYJ47D>WTGsGOBtjY#6uN{yaC6F*|9dA%69BV@-Qo|J^e}ht0+5ph^I-y~szG<{LjBeD(vf*LY#Ql}(EH0!RpqVN{$8z+o+#q`2G(B~ zLT(Zz4zar@V>krK_0et9#-HY_-~Z=40Qf%#0!xMKLgc__EOE-IsR&~9F%a36CL4O~ ztz`9wOJ;W?>$gQBb#ukxs48<4#u}uKk<~uemDAG9?`N$i6*twTr*v1 z9MF;1>05rBFW&@M<)Wr+qIdLWV#&j!>VFLt#`>6_g{>r6opxC^H8ufP`X&OvZSzV= zygf1N5vTlLuWj3Ywry1PG4!}f0sDunZ(|ETBAQmmrS#P-ai`ip8V#**d2F;VuROj; zwy$CQ{JC?T`Nqmm5*+SR=3J4@*igG&)z)m+`6#`0Z0V9c;O)(ad)kF&;l)AzCYbKi zXnYr{CbCm!Y;u8eqYa$U^E$YxECGB1zotwe6>AULdv*wIA}*24WD16tl=fnwWalYdMh zG@{K_d0a244Ht;}Jnf8m*W%5OIze|Q;8mTjP7&JI9k+`B+A(of_hb1HO^tTKRDJDP zpv(W6&^4NeKc$?JoTX+AEk4z^LW8`{l>@XzHFg9w0Z$udM2vk#%6KUj9!W@KC1RU2 zSrHz)!XOK>MI{2m8eZ63AsN4Uv4jk?mUKTcg)(;O*(hEOiwbl)-WQ7ih*3$Q1&dNJ zciza|aojTg-Q$2cKcBXMRJbL=#?3`qG7n>V2Q4PfG@yTM)ackFrX!Ln{t|14L_x!8 z6+VxVVe!GKy>0GgamzKQdKyXOOZ42Zcv__7r~#>iIolP=91A$Zq@hYUF)emvCdjUrh=GVmH&K#-Uv~&*F0- zzs?%qoE->0t1e&kCXPNMh(D!RTlM+GC{AE1Z^^rYoS+1b$RH}AVpI8 zM-R+u#*L_9ofAALPLPnV>$>y8@r1Z$BgT!|XRn8?x{t8$6EAxsKq&M3WBa^5T=<2R zwe>W8LyqVfg7Vh{rhFGGkZO1-BlpM7hVj?L?kP5hoL$8C1cmHbEcG^@>?uW&5#EJ_ zsDtvSBJA;c@M(GP-ZHRv831JkbP0h|qh25i2NLIpjDeZ=HVB-7~9x}K- zFe8m@4eF8F@jTYrLD$l?pnC##gUSVdqeSj;__lQFqVc>3+&ZP>l^B3j3(zyP`D-G6 zQ=Xuw^bm2kKRn-ijrD$gtfk*@Ej&+i@d}?aBo!V8$&gRJb9$Q_Trhgo3`s+#f^s z<7dClD){uxV1Q*Qyyada#I00aNN7pvT{)qDIr-FAiJ9so!Rs6qv>Zyduh zX{5o>FlwLx$TdpK!SgUWt4tul0w$#X>>i}+paj6Wve`0XVU;@p8Xa#NY91}NCrKm5bW-l8`WOH00v zsnDp#n4m}%B1IMvM3>OXgRH>rd?Ja$S3|0j#9N5gpaWtO4>jjk8(?~V`MNLk{`v-@ zpf;&R|0fYHGl>h+LlE7J+=)eS>0sR98}`_eW=pG=j+BRm1AibLSo99Ex+;@SeFZ#F z>G3s4h4lK6m}CKmve4_xtefY%^ZCWr+WhGn@)u@7}PFfoh4Y?C78NK3G(I|)zS@QHb1oHB1-`2?Jz6?5;dkctZ zJX9iR-B9}a>0A0;-jibu0wYbi3_=}{f*=yS4uVVU8iYj@>}y4)9FpnwF&&a=%hOAW z`h@udw>|dtN0%b2Jn6-JV||g{Vy-Av+-;_Xu(iW({_GJQu_h?bn~C|mDd{H^9km%S z-ch?7s;#;(Jev3w`7mGcHBq2vRko%Lz0ukE8C=V&Z~%IwK$zq6rQp~K1kf#YcV_OU zv9tz$>#T#&?pbsaWFR5bu(PAZYtjAv?q=_13Wc(;I>dou0IP2TK8jKvr`p%^W!dc{$iF2%K1bWtkmF4s6pdz*4 zwYPBMFtOT;$)DV=_aKj!s*^Hx$04Py;%^AL?cAX0TrD6wc?;EOrWk%tDmNSWUZ1$ zmhob9Bgq_bh~rV9CZ-<)fO%)(z9O7xacRj` zI!6YvB%X-sv!uW{B@C*8JU#4TNzGA7e&rHWKp-8&1aa7r$*^QXG8?3d16OEHBLR`s zat<@H%Sz!8a9Os?@x&R_V8FzuRLdVUHy@R@4*)(3Pakd4A2Uo9HFh)MGQX@#9gYVs zSjUSN?wI>{`WH1Ejjs#k_X*@KmITHoA~SfZ=uihI^E2@B;%(n~`?Y6(k8ilV`j7nJ z&Q|DfdCvG>Qmd zqhO|!CN#8d;%+!Y>%~Dy+u)!Vz`UU`9LGdnkgT=^_OF{HX1+$F{Y+-*uPgCKL^#0p zF;MK10iL0PO;SK;e+9S9t*(It>6;UPh{aL?J7$Ud>9S$&!T$d3v;#VZidJm|mU&RH z(BCK#)jxtQm#Rynz)NUvzz(ow@$#`3;nhll#=C|9omN^#-@)2Ag@76onFJY12sP?o z5$HNRy`Lu36!*2$k_1cnqvMl+PaMi1oy z;M`ks>(q#PbVK(dA{4N>6W|k!rlOb5??m^eGst0QricIaNv@3V{in?{9IBc%tmEoG?UD>AJCrp}m3j8{4csXvQ2gc?Kfg+`qQ< zU1v^p5H!>N^d%(L8^de5F~dBMr(cMhQG(__cC;@^a*$m+4YIxRsu?d~m_x7ml#wM8b#wzqun(`oeHJ*{bDoQFP zrFlEuDHfARpS|0>sEZgHuLp!mLg^rto6CvFwZkvAn#o~Sxx1|KynMbhy_Yun4V~yW z8mEMvxMfzKOq?jo(9D}CigrQvi@9_ygw4cW#(BoL;aJRSuWFS^Qx(<u|F|JPii*VRrb&|iEQA|+2i@0su_5W z8dhk>A-0QVT1h02?8<7m60mev{Puh1XZI7F-ydT-NsI*EOliRCb` z#)WLYGLM3LxR1AYQ$2evZjNN2%v`?XGgtv>cB=Q$_ApZJ^S~fw#bhfTUCv5{w1qTz z(zKFXTHd5~TD-naf$xj?LMDtSV}iO~CpvhyO^Z~Ffc4IN#=+gmc6-;^o1R0-lX~=- z9c%5+i!WDO+rWjhXZ;+O5aT`md1cbE(I!mqNMX`3B~mouaOY^0k-#rFR)VDIKHdpz zoct{kgYk2%puwPhx!ZbJ!I#9!+J;7Ep2uZi!HKK~Qt($kracnKTxx|mG^5yc3;WYk zv>Zv(xwsY&-At4y4iJ5btn30y??V^teT2sQS=?bCPF zk3RF?&bAd|Mot*_4$P?sG>hDVz(}RSNp-4SaL6g3GDCm$#P&zM98I(03(8HPT?6|< zX7_1sY%?w$uyzS_wpe((6Z-bSDJLxUQVud3*3=uQXWfKk8yHR6pk@OD{S2d~A2y6A*FxUhw=8E)wxl3TZGsVp3f2_r>8BSKk}#P%y&4bNu|M6v zMBL-ul}UlBPEY4u)?R!sFRwJ;0R+;HgvW<9GRjBp#8BFbIdd?)`4Z?9{6A34J82S* z)5WM%JGBT6&YrV^G`-jOIK?8+LkdLtXFOL{57-2}x!v0C!TXg`yhIxKUL}8#q4u^@ zgNC|))q44>nXOzm_TVsf9WD>{%^`3x5N_lZ{BQ_dTUnrM`gc%{jT z>(4VDc6lKnA)cgK*&%nKp$WUVpBa4r* z>Q2H8+ikUG@PyMgfaMoxeP*#q&Po8JPFqV0;f^2Ggh@fsEEY6DSy2%-zVKSwzSt1P zL<4z#0v5p_OP1kKFI#f83V*w~poEH&{(UB}FS-wiQ1OlE57{|%$ajr_QksOg4nw~fH3cZr|yZG zUH`o^ec!SN;u3-LjjW0vNY*Sm%<)_6aOB|aV#7^pj7Fq~!BUE|Q&#!FCS$4dBmhZ3 z)DL}+ud`R=17@#RY@>OiK`I@n#S80`SiqBrzM0rV>QGXQ)N)74m7{o7$l$~24+^Oq z+n0SC&ufJOb2i6uFWQx6fXyS7=Sc-9<{(yps*OU^q&H2_2)PoT@jH3?CiZ?G*nySG zM@K9D#|_TbZt0@U?=J0X`6 zctTpVXMClkWK;F5DX;9`cyWGR5sS8B`Dc^mI7v(iKR_csW<;pLoeR@3i_SS5MC7}U+-loWCGK-lBduIZ&T}aOc3@DoEz#nb zXtltCt$@#2qtA;K2PE>i0>gLFxht@3Sjo~|$XFv9>Jj0Btb={cD&_6P8~?FysXaPo z&?bJ*G`JccCP$JYRiI2FCb5p|s<;WpSXeUk5g{dfGMSHgP+JyiH9$j+o0f;Oov3H? zUT%BJ2wsDI5)0Bq=GcfY5y<4aq)fD^)dRym(^BuXTLX(ye>5mqy+kxpI~YNhJm~dl zM172Pz`PZK_hX09i}oAt`p6}cNF+B_c_jeq+;|CF=78xLcyG)^&xb)NobsHGgWMSZN(Bp?4r2rF%^KZN zCG7oAc6U+EKx^oG&r*9>Gh+gSC(V4Q2DLFY%!4Ug( zMw=;`0i$*i953XA_Z#dUNZRK;OkDlJ6sxsGWJ+4n2k7DLTIx+EQ`ZSuZzB>cuuUhC z3>r33TRv<}C#vHnb$^oh^h;F^%ZtCr+(Y{PHl;7D1-6dF?PH1J8+~8;Slp^*Z150# zG8;!W`;s}jYw>yqSYnck_lkg~-Cv{99Y&-{6-30s2j5ORxBfU7VW&=U>sz=>mTBM~ zlf_SqgI_S?<{yw~@wvgq^}zorVEZof$Y^>zJD?D^&U1UQi}+D4QY&>bhTr6!VT_!DpCSv%9Y$N6t#+QAfkd!(b z84h==+oTREajTYrqhg5gbWOa<{ad#QrH;}0ZUzP^?8NVFj?UF9ktEB9hUC)8b#EiA zw3D7u&~ih@>My?TfuejjOduGI(+&g%n5(qtK%sgJ)g*{P>^iR(PNWZbXh<{ohst>7 zBmylI1`U2VpVx+^KK}6F8^Jp`22I33MkPO5G5VMxEWS5}VHtV2Ntj{T^`T^TzU23h z6E190lH(|eiX+)wQj)~-c7hBKOOT{1&@qa)%RVz(N4uLtW$12yVb71noh4ier{!P| zSnIl`)pg>`77yJ+N%Mk3CWKmY{S%(fAJ2b!7BM7lMm=oQqw{%?7}c+3bWq3a9J(Sr zb9 zK$XgmP)WvP(=G?7z0M2(Os;*ue;DRM)ksGntWmBuKAk{U1PB^hBrr{lr?1!+`W76> z9)Z6BWYr_LqX-7PvUEm*i&8|iBBjNBv{1bXMPyizffGbBVyXk9LRgYXw|GEgD46gl zPXpu+hNsIwVBqy_Xi!^FgG;(GMWoiyxjGQ#NJgQQOqG#%acA6t1Zx7PM-8KZK&$9a z=!7HzeOuV{uTwVp`d=kDCB??>2*~-JP^MIbb$A3D9MO&Jl>|{gSH36pdbu(rXwnjN zN9RgQLC<^F_7d+7cYTP9`_VlrKp5p&`_`!JZr4uUi^!>t1)S=P_UaBcS8no@!%<8Q{e0s`yS>oo6E3su81h}h#Z zLms2wOQ`4R;0$TPbO@(^5SdrFIIfW{ zW~?HEeOs{dpodZtD4kG=40%D$2pL!7!aP-?ZBZy1Kz?gHDa6xl_%-$0a%-MYH_tgn&n2-_ZO^8*qc7apNp1 zh~EOosBc`hYCo-OsrgZftZfg^o4c)L_~E4!q2fTY4y99HosaER?O{7$>qoHj;;0?fzd-_xAJ}&B)naF0O;Y_^ed%f2i(%p>E(O)h#yRcL+7! z=d|tv)AcI1(iea54D{=?vklFX$=Z4L(FVR2p21wEP6Y(D#hnmuGRLfPl;icGS9!jk z%_vW}KbT-n^d`Sl+IU#6{E%g-Pmp+Bl3)ILk?x!?5uqr)r>~mh_Jk+Zr)^Hca!np(>sS^t1oUjV5L*hyLg?YAPUa# z8$pR%<=J_G&0tw{*7S}C|32-=dGe%>)0#jo_%>sow0LUwCSs&*W>!COTborj=1{4M zppTF_U=^$*5o+pI#}#%uvZUevLc0;-xtaNZoZ6N%@DL`WEd2h1v6)AG_DJlp>y+^= zjM1xQ>3eoBdKy-~8ONPwFWeUEt7$*0XDu@_SBjDmmgrs^0w0~d1#DUMhSG3o?6CMohNGvBO0ASYlr+sOQzgyIDb{6%f#WTf@OI+rZ*T-pjM)(|tmJjfL?2(B z)1>zOP+j=JMbS9JUOvAO-Oy-|bl%U23-vn2e-5lRc+ksW)mXbX?8uD9Kz`O1gKw)X zv#iNN|5jG(UJ7Ywfa?M`n8qUiKv^%(Jg$5L_>IP{RfU*UUAN3OQY|;l0wTXCm08DN zH_5#7d&u+q}YbQVr1{D|>^%6D@UlR5_Z@K^=f%l=c6 zhtq1us_snB;TQ#jfZdEmsg-K21LbOirIXcfd(?~7sLPCAbd80| z)Le*9j=&q-4k*}V%D1qa_z;pCKofTqPEX?RW||}LLqwPk+zA-%F@Jm5KH+8tV=v0G zQ{kKT52Cm2dt3E%(WVzN{fDq>KRNeWQ6g=Tp7bx-nt3Bxan6^nHP78uM_Sb>B^CAf zWkFA>fIw?&TYRgI$J*PvaX&N+)bJ+LO{V9YzBIp4Qv^X1lZh?SZe#RD!+*JQVC)B2 zP8ZW34l?k)f7Go1N5v_4|BN4&S3JX?U{&qE&>^4xAchx;y&;LeLqPkFe)K8%r(m`h zf(oAOE>&NC(Br|N{XZ=GmHZdSKm1Rw5kDa3bXJg%M6&O+^%5``p2QgA{1u6f`o7i6 zIMonLhOMCB{!3=~P=Fc(OQ2;40!;<#ATNX=s56TQ4+YO|qws`7#L(xj`4Y>70mzQMv-k-aAnveA1oVt1m8v!=2>fLn z`$-Jv1bz_6KNQ`&NEDlgKVAhGJVfDf2%hH72xE<6{(}r7v)lN>iR}h4eqnNu<&nW6 zvRVakj*x;kfU$Iz<)Quj{_gziv7zyMcr!^&+eaeR-|fVsaLa81j5`EKta(VGEB36r z_`Gzw&=@s=b8MPZ`!zG7aWYrZg+9O0e!{&ZGRawq#=7l?pVlzYG9Ii!XAr*sgE{|1&Y71#knU5;daA^Hh&0r`29n;|2(p|Uih=@Qm{v#zwzP!F>6{ThFboM+{ZKy z{;TGlaw9%*QSQ8t$!XZwlXPt zqF&x!;qi*v`1%K@?8rLJs_jb+2TGwu<#SGetcZz#S>>3ni;$-PnD zn#4wf?y_YZCJsH#NUW5PxsA~fq(&0lSEG^r%IWaHWg0fV520$Cr_S^mE&nEC4!ge!w2(CT5IuRgFpE6 z#ajo!<&9im>iazqWt-^)0XH+lCJ6;)qwT=1ow+=?$mrwUTd-#MOF60WG!uE43DaN= z1A03|A;r!;s)VIVANrb2BNPSNp2m7})*NuYnLEw|d%cNVn+Hcb4>F7IZ`N!fm(0S> z;)pq={PF_DnoH3vfxttj>CZwlxzoJdW!SzkA6w3o`kS(rwgu~3hH-|J%jitNGH@`N z$2(&Y?0)X4;`*Y6X=&_mw|-smSB5%XJ$-A1CAb?!mM;o?Iw+rmWj}m#{mP`fvH{Rb z`F|e#-FU94apYemkhBh8O|Kut?tbIdo4IB2Dac~Q-MUU)f+^IG*9l3X34X-`u$&d4 zvlZs$%15w=&Y~5~jwb`F>-Ax*w?-b&SvXVIcdnmj&-bTiGgnM44RWp~2k~eEDL>9*qiRp zngH{yXmvGpdVvdF#v}HeS#bY^{}?V)==9fYy@7#91}n?oFMnQ_G)QbD-lHh`-FrlS zr^Z6=_L5{_wqRA)xh!JWWgP`%Qtir)QnLKkBi;c@rmFPHM`amIwfF%lPDW%iFyXWP z#1XQB#t)QBe2eRA&BO*-a`Yaux6|=oG{3#Q=zUP#&BfpT9~`$@6bVLUBoT=JJf6@R zd@VbK)C_gD9?>i~C9zadEiIM`Behdu{a4CM1bYe>FgA$-dk|`1$#ARyl}X^2z-;1Z zPudU-gU!z>g=L2v*NKrA3PQGg7IpmT@=mWR2}Mi8k{jF6KYQ1y(VM<9VC7 zCAcq>FQ8g;!BO6OZzbW42^ES!G8pbpUs;v*C8NoDZxGoJ`dNIpvo}NeA_b0ppxy`- z%$px70T%Z&f)LP8SW1XOQ=2JB< zv8wew`*NmgBxQ4^f1Qkg{}2^7?x-@tP+(?9o2P(&6fkOt=S2;EV*-+$@k}kMQp_wu zWE_^|zke8OCV~a#pe~$6t>ABWA`a%p4?RT~0`2TaF@?yBZr@e@qaP9eK&AjxUNZ2n z-N*tyZvZgm7?gJcw_o)s@}hsc#chY{;~^WSJ5DQ}bXJx@nZhU3-N;>GO7RJG2m1d5 zb^rR`P`9^)&As`Rx5O_5GDI}ElX6IxR`77kfJ{;{S8CIcy^w9WcqDFkTTkQ%s=FSv z9zZOZ+6C)rMi{s}If6ap63~66029mHjV(PU0T~cS(d~%HivoHCHgz(?80jDeMo4ZQ z7Q@RTEPo~${lGvx<)w-#&$T$)LDZ8qPj}H9GeoI&V;pQOuW9w|Yh3M4$zCX9%j>E3 zCW0%Dcy<`h0b}168NE$Rk?b(LXcQ|!FrUdS3SmAHR|2|G~C@6_g;xccjJotSutTZB*A# zw>e3Lp{tHj5`hNF{NCl&{pvd#E0yJ*x>fjlG-M7FHwh-6qCL}_MZ26-kfre>t`liQ}6ha{V8I78P1LJ~&4vSEY7>qWW`0UWhtY{z^@l>F<)m3qlqiapvq__p6e>Gp?Bs%zPvU=0Y%qbs)ZYu3>+N_$YOG47J%ZBlmV+g$=olc9hZav za)|h7)!{t`pUT{M3&H<3t$cGdEO+Qi`y##?9&GN*^7&$CQp9R+!sNWQ!Ds2Xv38dI zmyGNRZO&J!eMq#HCuZgpVP=93H8QTc(y}7Fh>=a9Y_Il1fvt5n8>w(j7z2l+*)Jg@ z5qwP)Gpu9-5o_LAmgKU9#b(Vxb0=UxLmf&6B}e$257oB%lYeE)-dHmhP~cx;Wf_9z zOVky~r>5fnQuEGB(}e5^dplFsYA4m3$-C#zwl#?=W)t_l-d%p-5@06*P>k0!p8b<2 z3?I2+{PlOX3x{o73R2mZ=Sj$+LdT0e$8}zMqTpEyJ)ic&jJe0Wc;Y`vc5W-c)51qG z3!%~JShXTHJO+;m4Bh~w(L8;x0@S5L~Pn^R5zpa1rJ6a6C3^@$yQAl zXk_OVZ??l}{<|UYDb#9}t%zq!LxIr%)T{zGH~ zKagy9r_KatBcM&{(x7hM8n))-qP@B>oV?Pm^e?0iqg$?jPPhIKYi|`5SJSR*qYc5` zgS%Uh-~@MfcMBdICT_vqT|#gT?vh3W1PJaBB)IGDyx(5mTKoU^9%CJ>qdw{$bJnb? zr)t)9U;S5K;1u-c8S)Nheg6CW`dW-ba5cgz*zBU8w>bZI0BxEb&;qtY;NM zek_PagF&{{)J5OtonwFiL3ZG;vCkNvZM;U(Dv4jdzOn=;mFaLi0DZ5;@mqHj&1=6` z40yg*e&2j0h9%kjU`>56{M{g5A(H|u@4Yg9gAofl{=dB26k7f%(7WSAwhgs=pUF{R zMp$6`I6q;@^t6T~qD#wGH9ny>qdp3XAdJu4)c51G<#xB$HxSA<&aq+!O?;lw>7XJKr=fkrE z(9PUADm?k~Jllbfk*fnStn(u|z2F`oA)_4Jx7V~H822-rM&^DSovZcG!-`BKIY(fk z+kV+sV^A{p(a2r>3=Gc5NcqP8!_DW0>ORp$;dZqf&LjzG^~OA(%rZx}jw=0FXVqEAj#T-ztI!kDi|@a?m zH?EFV_ASKsJiJH;j{Qa`%2$&Q9-H~Km6TM3wgg{KgJ2_DLP_&GAh9)xe03fhs1A*s zIrP-eUAs0wVi!^&R8vb+!zzS8io6}AC924{_c(zm+{DyITBQ2Cp~CtZoP_aW!Z2YF z8jT&PV>vB&HF-Thz~3&YUrGc+RZxqH%$A3iAtLd6A%yGJP=@a0`>e_WsFgGAuTZ(8W(O7qE%>?+W_ zm@M+x2r6!lbi{0hw6}io4EMA7!Afc|9%a@9I^PX1P+j#t?%ok!@b8!3lMwrzJpJnF zJTk}wt8eI_^=Y1|PTmp;%cYr_36EcbXYjYdA4B3% z;TqZdV(fq|IpwR#;*o1 zYiDt!x61X#wXHHPbbT3HY0;>Pa#HLmwz77LPgpF?P(Hf)W2yk=^sf#bpD5Zigs!J* zGJ{V(6yyHu1?l%(qHLB$>5a3+8zWOMr`wm00M!q9)1bM6^o(q;MNA>4()6EeR5ol{ zHFK>sc31tYHfTOkInX`~*j*0c-)1im)?W;AUts>HXl}Dd;9oR%QimVoUz)r0LxV5g zidzE%D8vn0N>%I6dmOdOjdeY_O_R=+<3V#&d%Q!G`?p@t~v5 z)3clBKhG!I3+C)PXx(!E!D3TZ^_obsXP8tppvCCjb*JGXqVO=OrWZT8N}|xN%ysse znAFX1{94_--B|XxO+o4Q2Zij`X`=)GL4#v(uwm@azmGielyw&JZM z52=7c0fC5lzo3NNo85kE*EmHd%D?S1;Xmes3;BCkh5~w9b&Nv$MWHf;qbrn+z#*%n z;lX==(1M-(FENHuRbP4&PkQ4E>kUU()k@%xN1!YtYRVi*qOX_&1c>?*t!iFZ%~set zxOOE`=_+)x6*yFz&=;#=q{>(l#tk{37}{z|6wF_Znm?5#j&*9JBE_e9sypZ4}~ z;dIgP^wC#Tm*2ikMVrxGZ_nU2v}R7`j|nKtVy-b%OJ-~9-NOqPhJ#Y@Jw^+nVA2Nz z%^~OM7FLj`)DFUy1vcDtLqZ>e;VapjTNAD0t6k<1B{`^ zk(ks6j-#Zvdzk^TxM{&&g~UJ^`|{b4#XFy|5ZxMKkv&Z=qHZOFVZk&;Cb-mY{2dI` zSG87!N;?gHR^&cn0}$)q=aFvcaQ=82zm`2Y7Y>)6+5{EPN#ZYeS4BB8x^t6ExuNPg zy}ov2ot8b8@z~KJ?%4CgL~=WRYb8MN#|00xkyV@fJD{uD?pVCl_Y`3zAdyRy!?Gyj zro-s+(WUY+nb)TP64@Bd#W6?WW#cOnv^`6rIq6Ko+3~kxNAZ4TtL9o)np5I_Yc{TdS2N&c|idZ<0iU zp-%xI8k1kc-N)RkA<@^u*B#Q?jGAg`S$85Sh2dVO(}*H58#0z+A=-+v0aqMCr|);W z@pVG!T830*-}Zc)(s-S+c6vo7XF~Wc{!)H#q)q>-p7o`8;WpOPrsVM`{g2q)pkU3| z8!hiXdm^Yx*Kz3547u%2;^wpKA|@GLcKJF$_1WMzM-nR^3W?Txv?EfWb!q+L{q5=r z9$!@ntAKiwS0D5y*e0gI`K`pP=IcM;M>3%?bXN7a%ZQ*{aeKlt5#^FP3Aqj04OE%-4~Qmzcg)MW73*6691<+vTt@{S^TTYOHw4|&cA4bT-DNCfut#o zuW+!Y$YO4hMLvYCSi6>=4r$Vp5$%qk`^zAm+|>BKZ(!tQG4<}e4n+WYodo>}SYNlLU4KgFC}H&Ea^ zipAeR$am(u7PfWtmT&FaBAD}lk-;II`Nf%pg_eqy3s(0_@b9t`>n8jtsqL!x-k8#T0Z)`N+rma#5#+p0*6dyYeg1YOO-nNWwlOu(i zTD2#}JWJ&2<*?k!a9R*pCDVS69bjST54YLH_e*uU`I-*PsrK13PQFi; z04qxWQ^sLOe}c@@0YD&XHDAC&3&IHCXn(nkgdowTCNCAj_lnMdGAN#wQS00w&llq&+~@y2t%dXj-! z!pGl+xw&Jgd}+~RSQSo^#_|MT6tFa~z~-j}n!?GtCx`h4bt@!PDZakWx>V^*c#<7I z_c$nF3g5Xnk`@cgo$58}z|FCB5mO^Si*0bWGQU3wbIJ61ZFV{Ha0=-W5J5=a#WPogSc9b1Cm)qnHJ z2>UnHU5h5<4OzSDXKU%u&I9;=22FlXZ1a4K({y3sV#+n&?uHVVQLr$=;#*@ zs`->5jyc)nxf=4l;kh-9`)_V z?D36l6yZR<3tWN}9??Jh`JPS@6A|A!p2prL=c|wc=|TBCJxS%KUD642?g=`(T|j72 z|JgBt%HK{v-*_cgVqa)9iEbak}?^!L|ijL{;?kl~gEGD!<6{)Uc9h(@)q>&|ytz zsN#{z8{x&7u9LC<#Rv&nfa^O+xK-p&PcP_lm;KA>E6a4!mMh2n6f?e$v=AzN{Hid9 zjw@+pS-SD!rkZq;5rib~{U``74wB$l>vq1kMTDt$U`&%_qD&2+On5+Yzi~Y{ep$F) zn9PruxESRWjaD0GOTYZex zZl}BR;-Og$Oj?VUF}dH**82C~n>x#BHq&>;^xH3wC=VH3K{90u)Nc3=J+`N$BvE86 z@JT@h7AI72rIx(_%~u3bl~E0qi{{`Sm*AuXevHL2jFd?2`9m-RPN~HKUIBg3KQMWO z|FiScjYFg7b6&2hvyN#rmOjCdjudkm&+%w7={c9eF+ZfQTC|87DT~xcp<8^~tTHR8 z)A*UHTr}-j?=AFZst`|UDDao+L`8{vT86)0>m!tPG5|^q;<`{;aqLd_&1WSQftrl| z9A%s`A+z%%+OpHS5z>pFVQT(=B_Ml&j}4NK>M zxs#C9WQJ%UI*d4$p78wLjN=UKmmbK``xiP7saHmDS|feyztYZsLbZu2n3g6yKWl!l z=9C8wgjCc2u!AF2D~ZM?YewW=#~r;^EKXrGu8wEF^`Gjm>OLVQp&CXSRHHQGe`;_G zXlwKJ2xtOO%@`Dfi*wvq0vJ`EkG#W$GfzX1y}n=2AdFFAuUy~ z34F)Zd?3s2K-!f4@=Jd4nsea|3BQ^zX$;(Xh7x9&QFqi9}mXYJHb#3RHicZ@nkEIBdA8?W& zz52gTcK5R@80HxFEUzp=%t6XQrn^`Bn%^GkQ2@acE_Fp>RB#!jw2iNTgt6K6O=*>I z6@4+YqqxWu+d|_{ql_8J;s&unkplyefDzeugPzieEm!l}%hpTKf0yk32kfqN6C<+z zI``+w4@z4$l}cT2EE9m1Pbh3?g<(xew$RO$qMJwT)+$2_W7{VGdfhrlv8QSX8}yz# zwsZEPG(A)`sXSL9oL(hj9*Su<1h@P3X4%E}=y7#_7|o=VU5CRv80+A}nZoC}FADa<>9`lXY$vNik5Q?PU zqG)M@LOdHF8>%a!02h?K`>c6bPM1QGgb{>AnKnKABA%E*{U+|u)=iPPX zHy0ot4&6KSz&erWryh%wA3HJH7|jr@YLeGX*5XfRyS9p$y{g!1cD64@U^Z5^FI(TA zOiI%mq+02WmeksuNGizorl5vX-k5hAIR*$lU;lC6FIYwx#=$#086+^5@a`)-k>%1{ zgxR`8{xtP&z&i{0iB2;@kcDP0jxF6pN!^>lux6J{$`rwoLeY1BOu+ZI3X`}6%q{|a zm*_*c%gP%WBxVEHj*GKc4AnGO(UL~hmCFtL-g3&X7A2XX`ZvS)%)BGcJz4rUjC&2b zc4K*LnqS?uG?2<3gGFt)@YQu!Ex>LuMR6Q;e+!@vu;+hw`9igR=UhMg3eP-7VIT9=7=0eiR z2|Gm%^JI>XG1H*+udLvlRST8$`82KcNZQE3FY?_KN0?9_vlL}Dgeh=%`h&)-P#Fg-CvKfQ z;zfX@knI=DR+KRNNq&NlrnO@Xf&yp&pa)jY#->`CxI-HRAQssX4`^>dd*7B>+QX_UrByE zccJeCy=Gzc8MAZNFwJJV%!S2M;tq2b^$MyEpM-@DlDXe7OS4diSXiuLR%mur zr7nc1JE8Wf37dG|byboI2P`kO3SvRw;Vf}M-gM}u%zT&RP47CIf3U2r%oIeF(gijb z=-(vC}Q-$iBrKPyNsi&A#AQ z+h-6?4CnSNL@;0=c9l&Z-iEX5JfnM*o1%Fk2W|K(6ybLkWFz8U@iM2okNo-~^lKUB z-+=f3!FL1yUBH`t<|W|#GSczI$1RRKduUgT99iC&n(7bJACP!FMphvlBzE3L5mR1a ziGryACNxY2_fy7@XvReoTybC>VJP^^R~>#tDsV#{EW`mg?ZSrAf5I03dIhYl`HL() zMm2cTuq>Kkfura8u%y0liB+U&^o5!vbB)ECaNEP_>!$eFS>DyUNzoCxP z<^BcNDA;wzbh>$wEa?HOI}Zdh?|w*o_PnrZtrrvab2^369nHL4{*B1=Yaz9+Bxiz3 zOV$|QWrQ<@NUt*`%=yFkG^IBbOQYBf7mY@&USxzjRHJ!s8YG2bGRAgu&V#24*r8W*YG>R6ZP;bR(r~zvI=A;Hk4S6*4 z6P%~WDd?coxL(lOTwg~-+ErX|J~=xz0z0JZfT05oXg;t?`3dbeVYl_;1^s{l(xq!= zAG-$Pk6i6fy}CNueBIXUuXy5bI0@ZazGuvaC49Ec#2f85U`T?6JAsKSU{%pR_#z*$ zp0@nb*LOp_j__%>0PqcJ-}5!3I5Z!x*e|cHLoapZEy??;EuC%R7M-E_x@r1sy@rIm|~&TJ4rX>gw{o$_hBjQMF7ZPd798Lme#Z}5-@b0I81XP zI1bFLVJXs(aG4nLUDN_n`6xK&@7o(^`Rtj5H^@1^oBT~eg5)*p9aS;(Rd*gDg|EJs z^BEO-Yp7KaP%U|tKGdr@X5KP5J|CU*aRkqcvCtHyRc}xxynVTOd@(t?ix7G8=_7S3 z#g{VzdrRFw2M-QNyU^0q3>)!r4`cmrrurAsebuQeRJ@@zfI4-${3G%&|8w*yvrTuW{IU9M1YuwoF!14I zq@}y}59#uM578$7!o1Bil_lD9_GzOY3%>ijIwr{*+Mgjk*LuL~*AKCHOSx^g>H=Pl ziF~(3ExkYeF9{$L@BXRozoN&7L*oLb2Oou{STMHVy3G1{j!!#max~-+QW_^Vvnjc(%{`Eh^!lK`(ZY}iEi`oFmI(f%QPbG zFZ_I4uVswS5A#Me>WJbb6TXgOBDLrjZn;d91z2S@4mFwSPU-K+Eo*l4BG!xDQI zrN8X$YPXFWPNnf%DA&6d+SZo&Kg8Wk|A@PfR5?FM2}w1fIM_aV&Yr%bJCwdqQVN$J z(>{92O?c4XtX{qHp*8dS#V3Klf$Xee`fe zY#i8Ee&e)Bw%oH91_?8DseWyWRDP8JIO{d|u~wC6gOlgoe;lqiQ(V1|B!>1{PQUXus@pI%RTUpm3JU?kQkcC2Q@KB=EbX@-w7!KD4DL|B&^wCFr%N zEwM~hI)n$qh2hARpJlvwBUZR2lF?BN9s)??a!pED0ru2cLmFn~2U%}UXrq>7V`RfP zQv_0}huKzPu9Rpq9XTP)NhVfa%I8WkFe&Uw1oFrZ+Nk{fPKsOzvQ6_0UE#P%4)V0{ z^V!GFoZ~z3LFGO917eLZn4ikV387Plok505P}d)VH*WpY1lcU+muC9EWS&z?Z8okF@*rHkr^29pEH8 z@SgVlHKD(cLLD}#TPrh#U+Jb+qvPi~s*o~yHdfdw1JQY>8Su;`M_ZgC(dX9tpj*lc zGhIeOZVTSIJn9OvzDUiTm(}SuqeF+2eSlB#Eh*=W9F|1PQ+Pw$4A%;NOk$r=PdHOd zcHp+ZdF(+5br4jOUN=4#49(BG~YDc~7=+A8<6)(Fw0qa#5upxG%GD6-n`<-~xtj zSA01Ab5+ZrtdRQEhAUAySq6*{xhv}#;Etu6(mvGKv`CRS zsxl-}F1xB4<^P(uE3+*NQ|}dM+tOY>mHvAa8A~{mEI8`hUE{6ztQX*l=d89r@n;w5 zM1lx%3&Yx;>#Czi9ItvpxIRg%yV>J&ffTW?QfFmL)y%u zm^<&+no;eUl86cS6SsfRX0E|!-3fa)Ur|)z@#Lk?mSXy!s3{nsRU!x z^MNiKpgEAmOMhn}J>!NlRq3d{&^z%+Te%a}_A1bUsj9xfQ>}kJA`_97+IQCf^e23J02^Y@Z9aN>lfRS-ZeG=E+vB)$Y#;%vgob z1>G){jpmAi;ZUTeb+*9H-*%?z7JK>I&dk6($B)acu{0l6;9(@5QTu+Dsuorx@JW{m zV(y5(H|kHwi3aC)M+TU5^4-sFgIh|<7x!vVl20x10X3G!KX;a2^NR_0Kg+wz4k^op zZiXAT-Vw(A*=ledUKH{V!7fYNDxXTx^V4d;5%Hc07+hYk98#9z+Xg`?gxO(;a2+zE zoB34UU%g#5G~w4&ezGt)@3rRWSV`{QSF3)=@o0<4S9aGRp2Mg`I-fmz+-{@@Sb9W1 z$KJ9o1qP?88 z9?l!^I7U@3$9Ws+um@H0t1*TeNwb$LE(m1z;=MVtm)~$$5HU#X?W}AJRTYlPzClmJ zxHjI{^APi!O0nX_jnmZKV4mB%A@ENYTaPhF2jt@KH8L*(g&3;cp*dw1zQKC|6_K%3;7dNPmWWW}gY9(5OVrEG z;heIQ_x0tz2lQdNK{A^S99YSqPIwpwQPc`iAvhAa#zx|$gdSTPc*q}*@am?TRS0@Vo z&5Fj-$Lfh|6EWBj{WbkS+x_t~-Vp>*&$|!YqZ{7(kgfPH z+xTW}$R6;IYJ6(rqyL_i?9Z*Xj)5P6-ZRMDMLrO_xViC%B4GtVm?))O>=W!}ubNsq z$Asf?YoF##KXE}kbNggHxvzd4Q?CW%<&RZ3|B0Z7n-B_DRx6$Q{44r)RIJoPyE*uf zIlcG9YYJ*LY3yrUi0q?G4aNm#dKUcmuai14Rzvc0xCvDQcJxvuCcf6>x;oQ-V*4I# zVzn|gB%+x`JQbTMfs_lyMW@&mR7l61pm5q1%l)Q2Z_SPAqnePvBC`_~4eFAp)^}NY z%y6Z)Hm>4YH^z(@92S^G1m#{2OlDkbkR2;9IH^fx2HhZ0D36w41-M?J_)el)*jOq%ZguM1 zp$Pg*yM|v?Xoza)Epkz96RNO1O`T7my;M;EJcq>1S35fSt+RiRf2RkoT3P8(4&iHU z69_}Fz!ZK;%gp550LydD_^8WZe>3LzsVGeFJoJSqC19Ko&kvrp9uo!%?xV0Qp zFxEPTrx~3}hz1ctK~glyeX^RAjMyFFMe9g#BrYgm@}#lmE~-^2`w86eL3yi#s^b6e z9H%5UhR(kZnUcXQTbcprlx6c3Vzyr7iU3eIT` z2Q_K_OPF;`X=6fC%e=IK5()h{m{EV@R+ggO&ls@t?m(jyJLTrPqUmb%3bcSXh)K*hfkEJ>H<71{JGsSdcLX<57yHPfh z-Hb~kku<{*Lji?p^)o@Cix|WYq9=0S)#2qZ;+|!oruH|yqrFt{y)KS>)iH0!zDga} z1x{)Zoux?7qow<;_HjkFk@n7ZiW!uyBPt8#eU{E^tP}7~EH8xYb`A(zkSt~Lx=kx* z?K~)5hC{{a>A$S1lp&sf{Nr~2@{hy+7yo$QfBMHk|Mri2QpW$09vsUNLaq2vL6i~PqF z;vP4GxW~2sy2nkjCbTHmXO-h7jt&w9FMT*i{odwi7u!FKA`1phNSI9V-$*FzA64Gi zZnq~Iz9^~mLzM7^*&GR3zuc6(n{w!&8a;r(Oyxh6#zPir@)H?@7+>(uv!#(ona^wv za`x65x<${dSYrrPVBHomW_cwl^xn7yb)J{yUS@8VmV|dIRxYv);(q!iUtsxm8tXjk zS*v`(0Yy6AxV0o=amXeZoA%>O&n(xkk%UCB1{VC%ky6H&DPixDnZnY(djmvWEXPPd z;4jK9P$opp^-hNq_M6pkYEa=R(^B|JLt?=wk=ibyIEP{wd*i2+WO67o$dpxJW<_pp z)dU;qWx!e8f+gDMQ9!6^eJGbz!DPxP)|GS&RYZCABI4^b|C8rO^~e04o?y8-;YXPY zgiqh{8bt#W*gp_7*yCHqK!=q7vZ?}nULNU`;cm&B9l(dfO#E^sY5FX-2Qx$`lEo;a z*%UL93lq8QQDyTCbH!lyd${{jI}l$qch>=4a6VroJ^C(>YhXe0rGXY)nBRm;w-p=j z)_B(*iEYdMA`IqjGt&3~YcyRRK=O z`jFaIbKmBr;WDPrZ4=3jQi@9>w^&7e(xL`AD~Ja9pv)4b)^#&VGX{ z?T(el$x`*wp<##a*wnW-c@^gwgq<>%)`_LeJl57HHket`5-LIO5^Zzi+RJ*((b}{8!buOaGIs#&w-VgXTlgBj zlk_-wux>IvQtGC0#NTz@GLwmOPP~)&$q2`3bY<<{{JgggtA(>MlpYME2Lz*h3+AAK%UfhR*fAvyP zAyOSw4IHC51lxiprYAx(5`37mC#|o7x!<1DXdMu=#Fm+_+ySQV*!#tuC1^~qLv>Lj zL~g=q$*<2SB`gx~BCn+vo@yU}g9Qr|+z_%S<4?E|PBn9K#ctYS#w8Q4=`bYcAIdEc zMtcp6<1+j9N(D)*d?G=}kR9TUd8!kcQ56p_`g`6#a=w>OKim(ndw14JW}$Ys^B4-u zbPCSz^!97;0KjF4jHMWVe(wO})7pikTr1uob<=}f;m@lFj;WPPvraO%%&m$ylDb!+BRMgOl~1Nq>KJcd~zW_<%AUMZWk^ zz%{QkB6|zr{>G!#s$JnBIVp4K`@Eevfg+Vua9pBDOiyz-VGq`Q6genR+{FW`kR>%fN^9*7Jtdc9Jy^*tbHP zWOnDv#*U?TUA+tSy61V$5ie|fTi?qFy2thA%=_~(C2F|eA6@gh+a}lR0W(LQj@lRR z=SltLZ2M+)lZKfLg%Q?Mg)-O{*UWK2LuA*>L@@Ki=ZR1YSYli1hNtgg!{kLN>(ohO zCUEJ?Df?oqdDBJpytOc&?t?yO>M8YUg4BJzWOUx4sBV} zHit7AbQt)^h$t*^+gL;y5c=eyQwNK~a|aFHt|z!nG&6VLN0H42@V15fzoMKSGvkqs ze&tc~2`z0JCsOQftI~;O(jv z=jSzkH_kvpz868my+Z;AHGt&g*FN-Ur{I(Cp6E0YnpbxRF-o$RmC^X1K;aMnUqreHy{Jkv|XbBu`qPt{NKD3SPs& zZJrfZSYWC^z-Y}E@llB_$&T*_@{iBbSHZQl?|YWJV4H5UVY^b&bWpF7T(R;6imeN; zlK6e#y78$N7!CG`yM9=3BlslVK>4^t8F@?(ZSLK#-Cu7G3H^|_ou+b2O%1{wjU~<8 z66e~Z^2HF6Q#A=H*#TdqW~7i%mPoZ>B_|>1g6*DXfhPy6-^a-#S(VkU4d1}^mRx0# z1Um@U;Z0_jY4OgGd`!xU7mivIG&tu&LveZTzO{?%r|84|K#S4?N8fM@>WxBO19Gk; zRP=qGB#J~ypJfKx%PJdNC0g~S{@DzaMwlnd{17mrYT7Vw1Ytk({W$a})Tlv7L+;*z zUMcY!@bS}957elrx`D{ZZqAVzm0AD646y{) z!GM@T`Ut=!@7W13!=o87*ngEs8V1#a;W-H8fs;K{6cEZDNytYn52wWgm(ztmYv?9* zFm~60|N94IF=0VWL+E&tuFVESArv&rWCRWkG7asS5h%2jChR74n@~;!$A3m{op^b# zuJ6A1f>5+XhbY*8T~!I0v7d#sO`4!wRt2yZwVc6&i0!o>YKfJ4Q9+12L>s8VKy~$| z$Z}6UYe2rI5_F&3*fd?D2Ldt@Rbtae4&4V#w0Z;0YtxxbTL*W+kwj*ib^HQ{QVaTl z9EwCR2VOje-6wL|%1)klgUo_iAS*ngV#&ZvQOvDjS!;yG&GS8kMsf21>N76lfXvnyIO=3Z zxII%0+yvBD-ByJIyPdb`LmQ|DAOjwf4b=2{^46ER-3hr|?!YmUkG)jf`s?udVNDaI zb!666HV-=E9UZ+BQMa%^F2j$tu8<|%IWHo>Pnk#dvRl}~z+bz1i zPpR>BjFH`2h;?5@0aSW1iL{MX6NxW8hR|(6QN7`>dcPT14L8^~*!TI4U!?83qJw8g zU)rre&b7oo(yi-(y-Z-gb zRSdf)Y6bs(+%<6rj+v=xU0#$+PJ{!bW0lf4xVG@t?0X0TBQfe(_~=)hdJ)g~$LfVk z{DKCU(0pDp99-Ub@)|A>FksHY`FN00%+bFl%J5cPLurz?Y7?l-?I9FTi(QpU3~g35bh3G z+^*R(SfuNn-SH*n+$a?aG&k7FIQ)d)JiYpSJ?)6fSJt#sIW@{Lj8napREsnbg)WT# z-I-=xm|rm%8xsSI_rdfDJuE2U?vV#76Aa}fS|)$$NW47#ptZe)BU;vEP%4oyP0N{@ zQ)vMamB*BAxu!9pyt=f8cc zo=}1!QO}Yq(Tnaj%ls0<{5}dtRUlka#w$P_(ig{Fb@3QcdrP%lrqc^Fz~k-*sV9b=3*?paFBSI1L`*jVBFSdz#%jZZk*oL_W32za&(TnhH0 z_jY3vT3WfiNW%c#>r|NL8`7kz;m;DC=`^AQ?@^Rnphp!zULl+!9=xQ>KzGmc;^RH6 z3dm2Xh5m!B-|U#A0-06r;yyRzvgtoi&5@MV$5aJoY`ATK&3bS6U^&;q9&-4S%yk%q z>7rq1wu$|eu$9!2;r(En5lIw9t&5@8ek|f{^$?9jm;z_w;sspJ+`C6MoLmMj?pj}L zG|nwrWLD`V(*!1_Lvh1E&fJPdFAbn{&pXQFtc1X9TX@l7p@g?gf~z2BZ0mY?Je->P;;#z*9+oaMAa3k{ zdW@-su1Bg*Hmy84hJ$87Xh?RLPu&DI$6~t{T^mT={*!C*vt|L+guo=D^JLfbm+tK! zlcTogdbd|Zf<2p=)TEu~zTiTcr9HJ7+_KP&$ICCi?Xi}8wHuZ&=`5`KjFh^**OJVk zq`bpWnYS}}8Fb%-0;`y8(a8zZW%e9CI#svb{$c!bvn0y7p)xatNg{U{`1u$eDnhLW ztyD$R_AYx*#jF#G@{t6M2B&pNh(AOctmFtSDA#Lx(VkA%;Qac{&HW`r`@syAuZlY! zE0b_YMu|D?-5{wzg zq6%kJT0p`@ItQ3`AnoPuM(x#?u) zqNP{seezi_KJ;8NVhATRD0!oT02(j(zJeGD$>g)|uL~dXcKu}U5Ke%<-{;()^efUu zcOT*R64=C81g{Sk>yYA1Rh`^izJK~n)FL6QB!Ao0iegta3tz%m7K&QSL^M8vuNZ?4 z-5XlCK^g1Er7;imvZW5hA|L*D+ZBF&ns;OW?Gar>Z=K&HBl}zs=pehKFQAydZcP=a-V9&A&wof0V3Kw z)0lxMcga_7aDW-R*TWM@yYK{1u~MYCdA?2f2j7&D3 z{PO%w(s zF}{YVeZ`^xlS4kFcbZz9J>_*IySB z3TNLvL)htja~8u=Y0cdBhk^W;92k-3%#Fckw;~u(&eL494ovBy?KXUDXimXS!2Zi7 zkQW8;c;0`^k1*Q*;(bL210wdR2<%JYXQc}fbD#MHGPgtT;RYd#4=uo=@Vx9H8sa)m zspx-C&escM4`kmbuj+XTCeB@UZfu+bA;P{xfD9Mx{-mA?=zcA^h&qCb2F4J|Qe7_5 zU%uIbuN2I1AXw1fMM|HCW5{E)S2aS!Gyu!418w`@EfYf2v5U&PH((kjBS_w-+8aAs zmSwnF#%WOJw+ z>0u4mM8US=B#NMTf}V|kZdI#dGzG)h;Hz&T(!Op98OaDi4yw5sc0>v-bs&d%C!iTk z+BHC+AiCk#fXerGl^ADh_7u1)*^F4b&Kt4E|JKS16Fm_ly8b3-7q%%2p^t&dMA6%ft|4Ou zI=d7OcFmZ0)?5eo`;8}p5Ka4xaE{R};x75l4R%7WbgjJ`*8j!YTSnCtbP3uQcXtWF z-6goYYp|ffU4sTL?iMcY!QI{6-Q6_=hv4wtyl;2UbWit8&-&&MYjJL!bKz92y{c+I z`zf*kviMzjeMGKOX@I2(7u8`0DK>U5M6x2RN1vJSQr54Du7<_Ai|j4`u^gvp&m{Fu z(iq1WjIN!XLz-H#xvD?~pFdd)ugj+QxTr>1J>7aB#40vm-da<$uj0Vx1H`9Ap4BUD zIO~Jj6tkQu5nJC{?Mm9l<`fb%Ra}U%9(bj0Gt=@gjn++%BX&%;vpdq_Cdlqxl4|0M z?_1|~hH&+>0|3TpOJ(rlTtjtqm-*Lv!ap&v3P;=XsCp0vJA{@lT?ilbZ~S;dm0_DLcy@PjRGU z&#G}rlR!qReh%tm*72TT(_fmq)<4q(qp0LB#eGrHQ(T}MUFN>0O0|(S>*2P%JIn?5xCo?)34fSzQMT9Iw|ioK4*H=`CiA z*B*LM?~_N&1~7~#iV~B`*>A(o!^|HLf7^KX^*%iZR#jwY`16Ti__E|wp6GZ>=u?6d zN>P&#t}7yVNVS}}Bx1?yei%8YuWyG6n=|~!5nJs5eSm%+#HNVWyk|Md%ganhi5r24 zmQ|0`!0_@{ym;UzCK(hz^u3@uc?k4)Ure)JEjirsdD??MjvJM((t7$b+j)8X0eNEP zb&NxTkK;Y2#@{JmQi+YP;7cu219`f=<#K8{AK6m5bH+{iFmaRX^+$@L%cnY1E{kRG&juf%(VO8>mE}h(b zPfxSnwlRhkgvTWUpMOR7U|Mkx@we0DBMtUh=DbVq54I92JsSPQij~WwHQLG1VM$!E zSgkF2U)%;tca$DX^?=VQn6-X(dcf_~fIf+ib@B~+6)1Qh{2WA|~oCxwhd#?za7w#MA#1-Tbr z{o8_*#odn=cSr9|#KtS>wl~j&fzO?G^^7G9F+7RD3!hg>;*V2@gCdj(8>4O(E^ARO ztOr(-+Lq>c!UxHH$$7r6tHBSBbYlI`n#mG^5U=bT^M<<)U1eVHYQFDNtayauY@iv? z%X&pw)z&zcC>eqk=Z(g1639`wD*TpnSzy0r+gir6nmQqHWrY6wABzR6B}I3eq=JF4 zvZ+qs8S$>eqQCUl2j{f+MGJv6AKu-ya?eMNvO$hCZ|YLEN%qJVV3wm49R`Oo`Cy}g z;2WZyCZCHTh_$|*fA;Kj+hB%BE6?Qjo>cao@Zj-<2DP zmJZE`(PCLboU&sniw(xENb=mxayl17ii+ESBCX;+p3y)VG-rUAT4 zTYOjUyAJ-k-_0}KzJ&fTtRx{qlgjepTrD1@F`Rp&aTiE|GF(3xMk_!*TWD4OM(1=WZ*BI6SkAJn z$(k~rw|IUBT@RJ;X3vK)0kh3-hujF1b9V#il2Q?Ib^B9zqj=}Zpt4vdAl~D=3c6~^@e;DrD);BFu2#PpMw@f% z)`doiq+5MdL_TzY3ztOdDvbVk$Jd$;WY&Ak*xqpF4u;~Z$kmRxI@0J^^#1Bm^W8D@ zJ9PdYFZ<{;XN+Bj@v-ki8U*FGXnwO#=Q6P;zD;#nA1%32!)e0arggvcd+QybO;Gru z49N9NfgOka#m0uN20nE@0)`fxxHV8Ue)=7{_PavuIefY|?z5YS8VnkW0!8 z{lxTZxo#(51ePY>g0Ba0W$ScA(()5a|NcM|jbcDh!j~Ny>nE>^=AKlZ$BX2Fx}&W9TTlmD_k-XHElp%elVCQ1$?s_oiSbske=I36Df zP{Yj;(Oic3t;hFUp(Q&Bj-R8Io2^Ge+`Ax-hFXu6>$`)*l*gflc!<=Wd_Lf$eDD-) zciOO+{VknD(0)u@WbYI_+|^c)gWL8L)&A7l!Gx|CT6t{m$;Lw4)3&jf{s9FnN^TX; zXtXFs(e?vzY--O8=Y1P{RX0ZXMYPN>COG(3SNOE=;>;-ix#d|?4|bprA{dzTi3dG7 zQAUOA1sI~L#UuP|SjB^%(_n!)#AAvC*Zay*2x{qP%&@{HBxo{}I>y~oBc99*zG4PN zBd@a8;>y^%uD^9qy;6EsIDympZ@;n@LY(O#$#6O$zcYXQ9=|^+Brl%LN#TR}Y53Wt z|D7NhB9gtk2tJQB@Eyb!|3O$)hEsj?hp;DQZ+!1Xm=vnph5wg>{eZU0w z8J5Wpekr49F89LeFtXkj8Sl>)s6%rJF-M{Hp>Re+k4!P=dqpu?8dv-e9ADZBzr~Uj>pA<;a*g!{u&nqA?XfTV?Y5>uEnH zJoMt3L3^bkk0DP++#DFC!-3LK0A&3N)&;qKx=fQW@O>~^=j6LW^_0+z_ONiwpkr7 zaFSw`MAA5f&!ayKNUh!tpVc~A!g)!l2hxf&tj~n|5fXx@9z=;xv3(7FnE|Oh3{Zre zdNECbP5a@8md9q+EZ`!hOn3QD<}Vmcb*YeNlJM)6lCY*oU^EVW zsNcYvgiU{WOAoS&caHN3TU$JoSB@1%RU6X>Q35I;2Y_%1bU_p*TTtiY*OYlwjcbFY zoi$ZdgbGq(T0sd)AzQ+t8sf3rOr$Hg{qyTap?V6CGn^uRzX>TlCpQ+&Ka2wZ8PUt7 zS0yGdLpI?W8(Z(RJjC{QT|R zq7xLy&tjijjLbxqUp+?2+{Y89{q%Yl~v15BoHAlrlPkF}W#?Jv|7n zypqXr%c-Pn$^q++AiA@GBs`)~nX~bdeA+jmA~OGYV6K^k((qYENs{2S$GKYGX?w~s zAI=Zz8P4qZT$MdBg@t=$l^;)8&>$6NYVgX!E0Cg!OQa@!>HO9tMXf*_F=Mqx??ATCIXGe2fAmljQfHb)f}W)l4*_w+C(rB$~`BbIvZ0Bb+Flwg~7 z_O|#7yzee)u0X=t2tBy)_BGegV_L_07>D}?MLrwhT(`>Cqp|gHj<|G%$=-}d ze4t_k6umx}492qAte@jnN&Ud$W2DqC!G>|0Q35JPgq&Lr$0ZTKt7N75ssVbm(hx@E zI#4)l%^|R?n||Q!6{Fy6#Xq4S6j|VPJmj74=TF3R ziER-V#K;mz@`P}a*%GMfzvNT^wW+nQK%1(diYR@$=bm_)(C(3w^Vb$U2Tvg-;(<@! zuYQjNnlcUPtnXVr$P#w|*SnR|$OCv8fQT3edK3wVdkU&qul;wTVKc~fOm*-;8|#d4 zDt;cirSd1SVUXmNGL$={yRUY0D7!OnA7l&1RjHE02d1CSNJB;XRlm04Ve5%)OSr6G zcND8q0?+^feIC4EoaZyjyncbV-9x7Pfy{t}tcpVJ?G_t;OwGx(eR$KWysfEBka2%P z=3DyH+=X$6X~!R_?_|1y6;5Tq4%c5biBVUINet5eJ&uV;$E=B{FR|k9q~8BPLEpW{ z1K)`-M568%!QF6I)e0MiaUY(8aNJS!B0KGAj&S*DXtJ9+#eAB0Lm>r$Zg0g^C=W0H z#L>M$Zm8N0$oXM!5UtD~12=#oqQp&kqCBMUqqjI#oR^0vPvpC$rk987#Cz#`vajNx z3fn`^`)4huheO>8PCj>>TglZIAYC_xu&Zu-CGz;)K?;Gb!DydxVW?%b(Nw5~uqIep zFktO+ljAa1SLo(f4H0z()}U-j+W`U+0#WrE$y*e=UHCv>gs za{t%wulL+L4P|$CWna#!Y#f7*-4%vFlg@a+Ci-N&rP!mFX}5xza)yf38v6pnQn=qW zgErEqcdMHQ9bKNzn6?kCv$4vmXTSOQT6u+d_#ZYRgaJ#zPhn_7r3}#)`c?x*D=B?H zS6VZS9&IR=h2IT)c-G(VKow^-bsV>+vOa{u=zVrgnl>f;Q5CJY9m4$@TDIktR*MAuUPCju|u=L)}%dZESs>h`RfD zes{ns`uvKt^Y}fFO%~*d{E=ZGoO@jewkzMEOJy~mO{FllGO8}2-xb?o?l}reP#aV- zCM_=NA3Yrlnk++uL4Xz}m9)^@?Ms@;ofKOFQDW3D>9EF!eC zyQSL*QCQ*aY2NuMzwWVOuaD{wxZg3)5M$;1*#BQbOpjwqwErGrviW%aX{}*z&LCMI z3Y0i#%ID^3!TzB-am*A9TEwNtDwxlh*iusxEz3~+IFM%MgvBU;rum@Xm_O`Ol zR3pZH1T_T!%bII)9fd9hy_&cUucAbJRqGA&iWz%FS^pA*v2D50xx^sW&wNfaZSk-a zLAP{(%6uSiea!W8DSe;SS2=zXsCx{Rzqu3D<Tny0gfU??-r5g zkMdB`bBu|xGdzhSvg0@}F@M=?(O$A9+F3miDn=M5P7j7{^h8HI3X_&m7*RY0(?5Z1 zgiS6aA;CVU+R0D(+-Vm%cm&-$4Vq&e1K!*inqvPjpajPo12%x|?m;CWuoEMy z;0OlZ90q<+Fn||HNyg&+atFBYRsjjEu3P9q-gA7NG^(grOM$O|{X_RdSAQEs2jk>> z@BVt5hFADLh6u}rPmoU$o<#f<(Xp8qS`J?Fv02wQ7pE%^4@2+LcNi}^rW(Ryssl